From 1e6eb17a22056529601c8e413c2da0541d59d93b Mon Sep 17 00:00:00 2001 From: John Spurlock Date: Sun, 13 Jul 2014 11:59:50 -0400 Subject: [PATCH] QS: Introduce cast control panel. Make cast controller more granular, at the route level, and bind a common detail item panel to the available routes. Fix tile icon, add connecting label. Remove dialog presentation, no longer needed. Add the new empty state, and add controller state to dump. Found a bug in keyguard when in presentation mode, added a workaround - don't crash when no owner info in keyguard status view. Bug:15833210 Change-Id: I19afec390eac2260bccf4a7064065b666b7bceca --- .../android/keyguard/KeyguardStatusView.java | 3 +- .../res/drawable/ic_qs_cast_detail_empty.xml | 28 +++ .../SystemUI/res/drawable/ic_qs_cast_on.xml | 2 +- packages/SystemUI/res/values/strings.xml | 12 +- .../android/systemui/qs/tiles/CastTile.java | 186 ++++++++++++++---- .../statusbar/phone/PhoneStatusBar.java | 3 + .../statusbar/policy/CastController.java | 19 +- .../statusbar/policy/CastControllerImpl.java | 155 +++++++++++---- 8 files changed, 322 insertions(+), 86 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_cast_detail_empty.xml diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java index 38316ff9de903..150ab7411c8e9 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java @@ -86,7 +86,7 @@ public class KeyguardStatusView extends GridLayout { private void setEnableMarquee(boolean enabled) { if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); if (mAlarmStatusView != null) mAlarmStatusView.setSelected(enabled); - mOwnerInfo.setSelected(enabled); + if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled); } @Override @@ -131,6 +131,7 @@ public class KeyguardStatusView extends GridLayout { } private void updateOwnerInfo() { + if (mOwnerInfo == null) return; String ownerInfo = getOwnerInfo(); if (!TextUtils.isEmpty(ownerInfo)) { mOwnerInfo.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/res/drawable/ic_qs_cast_detail_empty.xml b/packages/SystemUI/res/drawable/ic_qs_cast_detail_empty.xml new file mode 100644 index 0000000000000..fbc21d4a43314 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_cast_detail_empty.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_qs_cast_on.xml b/packages/SystemUI/res/drawable/ic_qs_cast_on.xml index 8dacdc9809f0b..159bf65256c96 100644 --- a/packages/SystemUI/res/drawable/ic_qs_cast_on.xml +++ b/packages/SystemUI/res/drawable/ic_qs_cast_on.xml @@ -24,5 +24,5 @@ Copyright (C) 2014 The Android Open Source Project + android:pathData="M2.0,36.0l0.0,6.0l6.0,0.0C8.0,38.7 5.3,36.0 2.0,36.0zM2.0,28.0l0.0,4.0c5.5,0.0 10.0,4.5 10.0,10.0l4.0,0.0C16.0,34.3 9.7,28.0 2.0,28.0zM38.0,14.0L10.0,14.0l0.0,3.3c7.9,2.6 14.2,8.8 16.7,16.7L38.0,34.0L38.0,14.0zM2.0,20.0l0.0,4.0c9.9,0.0 18.0,8.1 18.0,18.0l4.0,0.0C24.0,29.8 14.1,20.0 2.0,20.0zM42.0,6.0L6.0,6.0c-2.2,0.0 -4.0,1.8 -4.0,4.0l0.0,6.0l4.0,0.0l0.0,-6.0l36.0,0.0l0.0,28.0L28.0,38.0l0.0,4.0l14.0,0.0c2.2,0.0 4.0,-1.8 4.0,-4.0L46.0,10.0C46.0,7.8 44.2,6.0 42.0,6.0z"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index b0f2133546e5e..6a7b450a3c812 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -526,8 +526,16 @@ Wi-Fi Off No saved networks available - - Cast screen + + Cast screen + + Casting + + Unnamed device + + Ready to cast + + No devices available Brightness diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 502713f6f9d4b..ab952c623a92d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -16,17 +16,23 @@ package com.android.systemui.qs.tiles; -import android.app.Dialog; +import android.content.Context; import android.content.Intent; -import android.media.MediaRouter; import android.provider.Settings; +import android.util.Log; import android.view.View; -import android.view.WindowManager; +import android.view.View.OnAttachStateChangeListener; +import android.view.ViewGroup; -import com.android.internal.app.MediaRouteDialogPresenter; import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItems; +import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.CastController; +import com.android.systemui.statusbar.policy.CastController.CastDevice; + +import java.util.LinkedHashMap; +import java.util.Set; /** Quick settings tile: Cast **/ public class CastTile extends QSTile { @@ -34,10 +40,17 @@ public class CastTile extends QSTile { new Intent(Settings.ACTION_WIFI_DISPLAY_SETTINGS); private final CastController mController; + private final CastDetailAdapter mDetailAdapter; public CastTile(Host host) { super(host); mController = host.getCastController(); + mDetailAdapter = new CastDetailAdapter(); + } + + @Override + public DetailAdapter getDetailAdapter() { + return mDetailAdapter; } @Override @@ -48,12 +61,13 @@ public class CastTile extends QSTile { @Override public void setListening(boolean listening) { if (mController == null) return; + if (DEBUG) Log.d(TAG, "setListening " + listening); if (listening) { mController.addCallback(mCallback); } else { + mController.setDiscovering(false); mController.removeCallback(mCallback); } - mController.setDiscovering(listening); } @Override @@ -65,61 +79,147 @@ public class CastTile extends QSTile { @Override protected void handleClick() { - mHandler.post(new Runnable() { - public void run() { - mHost.collapsePanels(); - mUiHandler.post(mShowDialog); - } - }); + showDetail(true); } @Override protected void handleUpdateState(BooleanState state, Object arg) { state.visible = true; - state.label = mContext - .getString(R.string.quick_settings_remote_display_no_connection_label); - if (arg instanceof CallbackInfo) { - final CallbackInfo cb = (CallbackInfo) arg; - if (cb.connectedRouteName != null) { - state.value = !cb.connecting; + state.label = mContext.getString(R.string.quick_settings_cast_title); + state.value = false; + final Set devices = mController.getCastDevices(); + boolean connecting = false; + for (CastDevice device : devices) { + if (device.state == CastDevice.STATE_CONNECTED) { + state.value = true; + state.label = getDeviceName(device); + } else if (device.state == CastDevice.STATE_CONNECTING) { + connecting = true; } } + if (!state.value && connecting) { + state.label = mContext.getString(R.string.quick_settings_connecting); + } state.iconId = state.value ? R.drawable.ic_qs_cast_on : R.drawable.ic_qs_cast_off; + mDetailAdapter.updateItems(devices); } - private static class CallbackInfo { - boolean enabled; - boolean connecting; - String connectedRouteName; + private String getDeviceName(CastDevice device) { + return device.name != null ? device.name + : mContext.getString(R.string.quick_settings_cast_device_default_name); } private final CastController.Callback mCallback = new CastController.Callback() { @Override - public void onStateChanged(boolean enabled, boolean connecting, - String connectedRouteName) { - final CallbackInfo info = new CallbackInfo(); // TODO pool - info.enabled = enabled; - info.connecting = connecting; - info.connectedRouteName = connectedRouteName; - refreshState(info); + public void onCastDevicesChanged() { + refreshState(); } }; - private final Runnable mShowDialog = new Runnable() { - private Dialog mDialog; + private final class CastDetailAdapter implements DetailAdapter, QSDetailItems.Callback { + private final LinkedHashMap mVisibleOrder = new LinkedHashMap<>(); + + private QSDetailItems mItems; + @Override - public void run() { - mDialog = MediaRouteDialogPresenter.createDialog(mContext, - MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, - new View.OnClickListener() { - @Override - public void onClick(View v) { - mDialog.dismiss(); - mHost.startSettingsActivity(WIFI_DISPLAY_SETTINGS); - } - }); - mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); - mDialog.show(); + public int getTitle() { + return R.string.quick_settings_cast_title; } - }; + + @Override + public Boolean getToggleState() { + return null; + } + + @Override + public Intent getSettingsIntent() { + return WIFI_DISPLAY_SETTINGS; + } + + @Override + public void setToggleState(boolean state) { + // noop + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mItems = QSDetailItems.convertOrInflate(context, convertView, parent); + mItems.setTagSuffix("Cast"); + if (convertView == null) { + if (DEBUG) Log.d(TAG, "addOnAttachStateChangeListener"); + mItems.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + if (DEBUG) Log.d(TAG, "onViewAttachedToWindow"); + } + + @Override + public void onViewDetachedFromWindow(View v) { + if (DEBUG) Log.d(TAG, "onViewDetachedFromWindow"); + mVisibleOrder.clear(); + } + }); + } + mItems.setEmptyState(R.drawable.ic_qs_cast_detail_empty, + R.string.quick_settings_cast_detail_empty_text); + mItems.setCallback(this); + updateItems(mController.getCastDevices()); + mController.setDiscovering(true); + return mItems; + } + + private void updateItems(Set devices) { + if (mItems == null) return; + Item[] items = null; + if (devices != null && !devices.isEmpty()) { + // if we are connected, simply show that device + for (CastDevice device : devices) { + if (device.state == CastDevice.STATE_CONNECTED) { + final Item item = new Item(); + item.icon = R.drawable.ic_qs_cast_on; + item.line1 = getDeviceName(device); + item.line2 = mContext.getString(R.string.quick_settings_connected); + item.tag = device; + item.canDisconnect = true; + items = new Item[] { item }; + break; + } + } + // otherwise list all available devices, and don't move them around + if (items == null) { + for (CastDevice device : devices) { + mVisibleOrder.put(device.id, device); + } + items = new Item[devices.size()]; + int i = 0; + for (String id : mVisibleOrder.keySet()) { + final CastDevice device = mVisibleOrder.get(id); + if (!devices.contains(device)) continue; + final Item item = new Item(); + item.icon = R.drawable.ic_qs_cast_off; + item.line1 = getDeviceName(device); + if (device.state == CastDevice.STATE_CONNECTING) { + item.line2 = mContext.getString(R.string.quick_settings_connecting); + } + item.tag = device; + items[i++] = item; + } + } + } + mItems.setItems(items); + } + + @Override + public void onDetailItemClick(Item item) { + if (item == null || item.tag == null) return; + final CastDevice device = (CastDevice) item.tag; + mController.startCasting(device); + } + + @Override + public void onDetailItemDisconnect(Item item) { + if (item == null || item.tag == null) return; + mController.stopCasting(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 9d7d933365d22..505af4493868f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -2322,6 +2322,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, if (mBluetoothController != null) { mBluetoothController.dump(fd, pw, args); } + if (mCastController != null) { + mCastController.dump(fd, pw, args); + } } private String hunStateToString(Entry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java index 54041e13ae00e..eb5804ac1e56c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java @@ -16,13 +16,30 @@ package com.android.systemui.statusbar.policy; +import java.util.Set; + public interface CastController { void addCallback(Callback callback); void removeCallback(Callback callback); void setDiscovering(boolean request); void setCurrentUserId(int currentUserId); + Set getCastDevices(); + void startCasting(CastDevice device); + void stopCasting(); public interface Callback { - void onStateChanged(boolean enabled, boolean connecting, String connectedRouteName); + void onCastDevicesChanged(); + } + + public static final class CastDevice { + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + + public String id; + public String name; + public String description; + public int state = STATE_DISCONNECTED; + public Object tag; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java index bcd865c350413..22179e0542787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java @@ -16,30 +16,55 @@ package com.android.systemui.statusbar.policy; +import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; + import android.content.Context; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Set; +import java.util.UUID; /** Platform implementation of the cast controller. **/ public class CastControllerImpl implements CastController { + private static final String TAG = "CastController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; private final ArrayList mCallbacks = new ArrayList(); private final MediaRouter mMediaRouter; + private final ArrayMap mRoutes = new ArrayMap<>(); + private final Object mDiscoveringLock = new Object(); - private boolean mEnabled; - private boolean mConnecting; - private String mConnectedRouteName; + private boolean mDiscovering; public CastControllerImpl(Context context) { + mContext = context; mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + if (DEBUG) Log.d(TAG, "new CastController()"); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CastController state:"); + pw.print(" mDiscovering="); pw.println(mDiscovering); + pw.print(" mCallbacks.size="); pw.println(mCallbacks.size()); + pw.print(" mRoutes.size="); pw.println(mRoutes.size()); + for (int i = 0; i < mRoutes.size(); i++) { + final RouteInfo route = mRoutes.valueAt(i); + pw.print(" "); pw.println(routeToString(route)); + } } @Override public void addCallback(Callback callback) { mCallbacks.add(callback); - fireStateChanged(callback); + fireOnCastDevicesChanged(callback); } @Override @@ -49,12 +74,16 @@ public class CastControllerImpl implements CastController { @Override public void setDiscovering(boolean request) { - if (request) { - mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, - mMediaCallback, - MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); - } else { - mMediaRouter.removeCallback(mMediaCallback); + synchronized (mDiscoveringLock) { + if (mDiscovering == request) return; + mDiscovering = request; + if (DEBUG) Log.d(TAG, "setDiscovering: " + request); + if (request) { + mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback, + MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); + } else { + mMediaRouter.removeCallback(mMediaCallback); + } } } @@ -63,63 +92,113 @@ public class CastControllerImpl implements CastController { mMediaRouter.rebindAsUser(currentUserId); } + @Override + public Set getCastDevices() { + final ArraySet devices = new ArraySet(); + synchronized(mRoutes) { + for (RouteInfo route : mRoutes.values()) { + final CastDevice device = new CastDevice(); + device.id = route.getTag().toString(); + final CharSequence name = route.getName(mContext); + device.name = name != null ? name.toString() : null; + final CharSequence description = route.getDescription(); + device.description = description != null ? description.toString() : null; + device.state = route.isConnecting() ? CastDevice.STATE_CONNECTING + : route.isSelected() ? CastDevice.STATE_CONNECTED + : CastDevice.STATE_DISCONNECTED; + device.tag = route; + devices.add(device); + } + } + return devices; + } + + @Override + public void startCasting(CastDevice device) { + if (device == null || device.tag == null) return; + final RouteInfo route = (RouteInfo) device.tag; + if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route)); + mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route); + } + + @Override + public void stopCasting() { + if (DEBUG) Log.d(TAG, "stopCasting"); + mMediaRouter.getDefaultRoute().select(); + } + private void updateRemoteDisplays() { - final MediaRouter.RouteInfo connectedRoute = mMediaRouter.getSelectedRoute( - MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY); - boolean enabled = connectedRoute != null - && connectedRoute.matchesTypes(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY); - boolean connecting; - if (enabled) { - connecting = connectedRoute.isConnecting(); - } else { - connecting = false; - enabled = mMediaRouter.isRouteAvailable(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, - MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE); + synchronized(mRoutes) { + mRoutes.clear(); + final int n = mMediaRouter.getRouteCount(); + for (int i = 0; i < n; i++) { + final RouteInfo route = mMediaRouter.getRouteAt(i); + if (!route.isEnabled()) continue; + if (!route.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) continue; + ensureTagExists(route); + mRoutes.put(route.getTag().toString(), route); + } + final RouteInfo selected = mMediaRouter.getSelectedRoute(ROUTE_TYPE_REMOTE_DISPLAY); + if (selected != null && !selected.isDefault()) { + ensureTagExists(selected); + mRoutes.put(selected.getTag().toString(), selected); + } } - - String connectedRouteName = null; - if (connectedRoute != null) { - connectedRouteName = connectedRoute.getName().toString(); - } - synchronized(mCallbacks) { - mEnabled = enabled; - mConnecting = connecting; - mConnectedRouteName = connectedRouteName; - } - fireStateChanged(); + fireOnCastDevicesChanged(); } - private void fireStateChanged() { + private void ensureTagExists(RouteInfo route) { + if (route.getTag() == null) { + route.setTag(UUID.randomUUID().toString()); + } + } + + private void fireOnCastDevicesChanged() { for (Callback callback : mCallbacks) { - fireStateChanged(callback); + fireOnCastDevicesChanged(callback); } } - private void fireStateChanged(Callback callback) { - synchronized(mCallbacks) { - callback.onStateChanged(mEnabled, mConnecting, mConnectedRouteName); - } + private void fireOnCastDevicesChanged(Callback callback) { + callback.onCastDevicesChanged(); + } + + private static String routeToString(RouteInfo route) { + if (route == null) return null; + final StringBuilder sb = new StringBuilder().append(route.getName()).append('/') + .append(route.getDescription()).append('@').append(route.getDeviceAddress()) + .append(",status=").append(route.getStatus()); + if (route.isDefault()) sb.append(",default"); + if (route.isEnabled()) sb.append(",enabled"); + if (route.isConnecting()) sb.append(",connecting"); + if (route.isSelected()) sb.append(",selected"); + return sb.append(",id=").append(route.getTag()).toString(); } private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() { @Override public void onRouteAdded(MediaRouter router, RouteInfo route) { + if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route)); updateRemoteDisplays(); } @Override public void onRouteChanged(MediaRouter router, RouteInfo route) { + if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route)); updateRemoteDisplays(); } @Override public void onRouteRemoved(MediaRouter router, RouteInfo route) { + if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route)); updateRemoteDisplays(); } @Override public void onRouteSelected(MediaRouter router, int type, RouteInfo route) { + if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route)); updateRemoteDisplays(); } @Override public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) { + if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route)); updateRemoteDisplays(); } };