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(); } };