A device that supports live video routing will allow a mirrored version + * of the device's primary display or a customized + * {@link android.app.Presentation Presentation} to be routed to supported destinations.
+ * + *Once initiated, display mirroring is transparent to the application. + * While remote routing is active the application may use a + * {@link android.app.Presentation Presentation} to replace the mirrored view + * on the external display with different content.
+ */ + public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + /** * Route type flag for application-specific usage. * @@ -219,7 +249,7 @@ public class MediaRouter { * @hide for use by framework routing UI */ public RouteInfo getSystemAudioRoute() { - return sStatic.mDefaultAudio; + return sStatic.mDefaultAudioVideo; } /** @@ -296,7 +326,8 @@ public class MediaRouter { } static void selectRouteStatic(int types, RouteInfo route) { - if (sStatic.mSelectedRoute == route) return; + final RouteInfo oldRoute = sStatic.mSelectedRoute; + if (oldRoute == route) return; if ((route.getSupportedTypes() & types) == 0) { Log.w(TAG, "selectRoute ignored; cannot select route with supported types " + typesToString(route.getSupportedTypes()) + " into route types " + @@ -306,7 +337,7 @@ public class MediaRouter { final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && - (route == btRoute || route == sStatic.mDefaultAudio)) { + (route == btRoute || route == sStatic.mDefaultAudioVideo)) { try { sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); } catch (RemoteException e) { @@ -314,10 +345,21 @@ public class MediaRouter { } } - if (sStatic.mSelectedRoute != null) { + final WifiDisplay activeDisplay = + sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay(); + final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null; + final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null; + if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) { + if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) { + sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress); + } else if (activeDisplay != null && !newRouteHasAddress) { + sStatic.mDisplayService.disconnectWifiDisplay(); + } + } + + if (oldRoute != null) { // TODO filter types properly - dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(), - sStatic.mSelectedRoute); + dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute); } sStatic.mSelectedRoute = route; if (route != null) { @@ -326,6 +368,22 @@ public class MediaRouter { } } + /** + * Compare the device address of a display and a route. + * Nulls/no device address will match another null/no address. + */ + static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) { + final boolean routeHasAddress = info != null && info.mDeviceAddress != null; + if (display == null && !routeHasAddress) { + return true; + } + + if (display != null && routeHasAddress) { + return display.getDeviceAddress().equals(info.mDeviceAddress); + } + return false; + } + /** * Add an app-specified route for media to the MediaRouter. * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} @@ -419,7 +477,7 @@ public class MediaRouter { if (info == sStatic.mSelectedRoute) { // Removing the currently selected route? Select the default before we remove it. // TODO: Be smarter about the route types here; this selects for all valid. - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudioVideo); } if (!found) { sStatic.mCategories.remove(removingCat); @@ -444,7 +502,8 @@ public class MediaRouter { if (info == sStatic.mSelectedRoute) { // Removing the currently selected route? Select the default before we remove it. // TODO: Be smarter about the route types here; this selects for all valid. - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER, + sStatic.mDefaultAudioVideo); } if (!found) { sStatic.mCategories.remove(removingCat); @@ -611,20 +670,151 @@ public class MediaRouter { if (selectedRoute == null) return; if (selectedRoute == sStatic.mBluetoothA2dpRoute || - selectedRoute == sStatic.mDefaultAudio) { + selectedRoute == sStatic.mDefaultAudioVideo) { dispatchRouteVolumeChanged(selectedRoute); } else if (sStatic.mBluetoothA2dpRoute != null) { try { dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? - sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudio); + sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo); } catch (RemoteException e) { Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); } } else { - dispatchRouteVolumeChanged(sStatic.mDefaultAudio); + dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo); } } + static void updateWifiDisplayStatus(WifiDisplayStatus newStatus) { + final WifiDisplayStatus oldStatus = sStatic.mLastKnownWifiDisplayStatus; + + // TODO Naive implementation. Make this smarter later. + boolean needScan = false; + WifiDisplay[] oldDisplays = oldStatus != null ? + oldStatus.getRememberedDisplays() : new WifiDisplay[0]; + WifiDisplay[] newDisplays = newStatus.getRememberedDisplays(); + WifiDisplay[] availableDisplays = newStatus.getAvailableDisplays(); + + for (int i = 0; i < newDisplays.length; i++) { + final WifiDisplay d = newDisplays[i]; + final WifiDisplay oldRemembered = findMatchingDisplay(d, oldDisplays); + if (oldRemembered == null) { + addRoute(makeWifiDisplayRoute(d)); + needScan = true; + } else { + final boolean available = findMatchingDisplay(d, availableDisplays) != null; + final RouteInfo route = findWifiDisplayRoute(d); + updateWifiDisplayRoute(route, d, available, newStatus); + } + } + for (int i = 0; i < oldDisplays.length; i++) { + final WifiDisplay d = oldDisplays[i]; + final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); + if (newDisplay == null) { + removeRoute(findWifiDisplayRoute(d)); + } + } + + if (needScan) { + sStatic.mDisplayService.scanWifiDisplays(); + } + + sStatic.mLastKnownWifiDisplayStatus = newStatus; + } + + static RouteInfo makeWifiDisplayRoute(WifiDisplay display) { + final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); + newRoute.mDeviceAddress = display.getDeviceAddress(); + newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; + newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; + newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; + newRoute.mStatus = sStatic.mResources.getText( + com.android.internal.R.string.media_route_status_connecting); + newRoute.mEnabled = false; + + newRoute.mName = makeWifiDisplayName(display); + return newRoute; + } + + static String makeWifiDisplayName(WifiDisplay display) { + String name = display.getDeviceAlias(); + if (TextUtils.isEmpty(name)) { + name = display.getDeviceName(); + } + return name; + } + + private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display, + boolean available, WifiDisplayStatus wifiDisplayStatus) { + final boolean isScanning = + wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING; + + boolean changed = false; + int newStatus = RouteInfo.STATUS_NONE; + + if (available) { + newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE; + } else { + newStatus = RouteInfo.STATUS_NOT_AVAILABLE; + } + + if (display.equals(wifiDisplayStatus.getActiveDisplay())) { + final int activeState = wifiDisplayStatus.getActiveDisplayState(); + switch (activeState) { + case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: + newStatus = RouteInfo.STATUS_NONE; + break; + case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: + newStatus = RouteInfo.STATUS_CONNECTING; + break; + case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED: + Log.e(TAG, "Active display is not connected!"); + break; + } + } + + final String newName = makeWifiDisplayName(display); + if (route.getName().equals(newName)) { + route.mName = newName; + changed = true; + } + + changed |= route.mEnabled != available; + route.mEnabled = available; + + changed |= route.setStatusCode(newStatus); + + if (changed) { + dispatchRouteChanged(route); + } + + if (!available && route == sStatic.mSelectedRoute) { + // Oops, no longer available. Reselect the default. + final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; + selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); + } + } + + private static WifiDisplay findMatchingDisplay(WifiDisplay address, WifiDisplay[] displays) { + for (int i = 0; i < displays.length; i++) { + final WifiDisplay d = displays[i]; + if (d.equals(address)) { + return d; + } + } + return null; + } + + private static RouteInfo findWifiDisplayRoute(WifiDisplay d) { + final int count = sStatic.mRoutes.size(); + for (int i = 0; i < count; i++) { + final RouteInfo info = sStatic.mRoutes.get(i); + if (d.getDeviceAddress().equals(info.mDeviceAddress)) { + return info; + } + } + return null; + } + /** * Information about a media route. */ @@ -644,6 +834,18 @@ public class MediaRouter { int mPlaybackStream = AudioManager.STREAM_MUSIC; VolumeCallbackInfo mVcb; + String mDeviceAddress; + boolean mEnabled = true; + + // A predetermined connection status that can override mStatus + private int mStatusCode; + + static final int STATUS_NONE = 0; + static final int STATUS_SCANNING = 1; + static final int STATUS_CONNECTING = 2; + static final int STATUS_AVAILABLE = 3; + static final int STATUS_NOT_AVAILABLE = 4; + private Object mTag; /** @@ -710,6 +912,34 @@ public class MediaRouter { return mStatus; } + /** + * Set this route's status by predetermined status code. If the caller + * should dispatch a route changed event this call will return true; + */ + boolean setStatusCode(int statusCode) { + if (statusCode != mStatusCode) { + mStatusCode = statusCode; + int resId = 0; + switch (statusCode) { + case STATUS_SCANNING: + resId = com.android.internal.R.string.media_route_status_scanning; + break; + case STATUS_CONNECTING: + resId = com.android.internal.R.string.media_route_status_connecting; + break; + case STATUS_AVAILABLE: + resId = com.android.internal.R.string.media_route_status_available; + break; + case STATUS_NOT_AVAILABLE: + resId = com.android.internal.R.string.media_route_status_not_available; + break; + } + mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; + return true; + } + return false; + } + /** * @return A media type flag set describing which types this route supports. */ @@ -866,6 +1096,13 @@ public class MediaRouter { return mVolumeHandling; } + /** + * @return true if this route is enabled and may be selected + */ + public boolean isEnabled() { + return mEnabled; + } + void setStatusInt(CharSequence status) { if (!status.equals(mStatus)) { mStatus = status; @@ -881,7 +1118,6 @@ public class MediaRouter { sStatic.mHandler.post(new Runnable() { @Override public void run() { - //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value); if (mVcb != null) { if (direction != 0) { mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); @@ -1400,6 +1636,7 @@ public class MediaRouter { int mNameResId; int mTypes; final boolean mGroupable; + boolean mIsSystem; RouteCategory(CharSequence name, int types, boolean groupable) { mName = name; @@ -1486,6 +1723,14 @@ public class MediaRouter { return mGroupable; } + /** + * @return true if this is the category reserved for system routes. + * @hide + */ + public boolean isSystem() { + return mIsSystem; + } + public String toString() { return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + " groupable=" + mGroupable + " }"; @@ -1671,7 +1916,6 @@ public class MediaRouter { } static class VolumeChangeReceiver extends BroadcastReceiver { - @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { @@ -1689,6 +1933,15 @@ public class MediaRouter { } } } + } + static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) { + updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra( + DisplayManager.EXTRA_WIFI_DISPLAY_STATUS)); + } + } } }