diff options
author | Jeff Brown <jeffbrown@google.com> | 2013-11-07 00:30:16 -0800 |
---|---|---|
committer | Jeff Brown <jeffbrown@google.com> | 2013-11-07 03:25:37 -0800 |
commit | 69b07161bebdb2c726e3a826c2268866f1a94517 (patch) | |
tree | 8d9c94f32a045a8f5c48ca0f1380abc760eac807 /media | |
parent | f3c99e883f46c56e5e2877e844b902b6eb45545b (diff) | |
download | frameworks_base-69b07161bebdb2c726e3a826c2268866f1a94517.zip frameworks_base-69b07161bebdb2c726e3a826c2268866f1a94517.tar.gz frameworks_base-69b07161bebdb2c726e3a826c2268866f1a94517.tar.bz2 |
Add media router service and integrate with remote displays.
This change adds a new media router service whose purpose is to track
global state information associated with media routes. This service
publishes routes to the media router instance in application processes
and handles requested state changes such as selecting or unselecting
global routes. The service also binds to remote display provider
services which can offer new remote display routes to the system.
Includes a test application for manually verifying certain aspects
of the operation of the media router service.
The remote display provider interface is essentially a stripped down
media route provider interface as defined in the support library
media router implementation. For now, it is designed to be used only
by first parties to publish remote display routes to the system so
it is not exposed as public API in the SDK. In the future, the remote
display provider interface will most likely be deprecated and replaced
with a more featureful media route provider interface for third
party integration, similar to what is in the support library today.
Further patch sets integrate these new capabilities into the System UI
and Settings for connecting remote displays.
Bug: 11257292
Change-Id: I31109f23f17b474d17534d0f5f4503e388b081c2
Diffstat (limited to 'media')
-rw-r--r-- | media/java/android/media/IMediaRouterClient.aidl | 24 | ||||
-rw-r--r-- | media/java/android/media/IMediaRouterService.aidl | 35 | ||||
-rw-r--r-- | media/java/android/media/MediaRouter.java | 542 | ||||
-rw-r--r-- | media/java/android/media/MediaRouterClientState.aidl | 18 | ||||
-rw-r--r-- | media/java/android/media/MediaRouterClientState.java | 183 |
5 files changed, 709 insertions, 93 deletions
diff --git a/media/java/android/media/IMediaRouterClient.aidl b/media/java/android/media/IMediaRouterClient.aidl new file mode 100644 index 0000000..9640dcb --- /dev/null +++ b/media/java/android/media/IMediaRouterClient.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2013 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 android.media; + +/** + * {@hide} + */ +oneway interface IMediaRouterClient { + void onStateChanged(); +} diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl new file mode 100644 index 0000000..f8f5fdf --- /dev/null +++ b/media/java/android/media/IMediaRouterService.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 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 android.media; + +import android.media.IMediaRouterClient; +import android.media.MediaRouterClientState; + +/** + * {@hide} + */ +interface IMediaRouterService { + void registerClientAsUser(IMediaRouterClient client, String packageName, int userId); + void unregisterClient(IMediaRouterClient client); + + MediaRouterClientState getState(IMediaRouterClient client); + + void setDiscoveryRequest(IMediaRouterClient client, int routeTypes, boolean activeScan); + void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit); + void requestSetVolume(IMediaRouterClient client, String routeId, int volume); + void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction); +} diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 9a79c94..c184e8f 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -16,6 +16,8 @@ package android.media; +import com.android.internal.util.Objects; + import android.app.ActivityThread; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,6 +32,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -52,14 +55,17 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class MediaRouter { private static final String TAG = "MediaRouter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); static class Static implements DisplayManager.DisplayListener { // Time between wifi display scans when actively scanning in milliseconds. private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000; + final Context mAppContext; final Resources mResources; final IAudioService mAudioService; final DisplayManager mDisplayService; + final IMediaRouterService mMediaRouterService; final Handler mHandler; final CopyOnWriteArrayList<CallbackInfo> mCallbacks = new CopyOnWriteArrayList<CallbackInfo>(); @@ -79,6 +85,13 @@ public class MediaRouter { WifiDisplayStatus mLastKnownWifiDisplayStatus; boolean mActivelyScanningWifiDisplays; + int mDiscoveryRequestRouteTypes; + boolean mDiscoverRequestActiveScan; + + int mCurrentUserId = -1; + IMediaRouterClient mClient; + MediaRouterClientState mClientState; + final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() { @Override public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { @@ -101,6 +114,7 @@ public class MediaRouter { }; Static(Context appContext) { + mAppContext = appContext; mResources = Resources.getSystem(); mHandler = new Handler(appContext.getMainLooper()); @@ -109,6 +123,9 @@ public class MediaRouter { mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE); + mMediaRouterService = IMediaRouterService.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); + mSystemCategory = new RouteCategory( com.android.internal.R.string.default_audio_route_category_name, ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false); @@ -146,10 +163,13 @@ public class MediaRouter { updateAudioRoutes(newAudioRoutes); } + // Bind to the media router service. + rebindAsUser(UserHandle.myUserId()); + // Select the default route if the above didn't sync us up // appropriately with relevant system state. if (mSelectedRoute == null) { - selectRouteStatic(mDefaultAudioVideo.getSupportedTypes(), mDefaultAudioVideo); + selectDefaultRouteStatic(); } } @@ -197,7 +217,7 @@ public class MediaRouter { dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); } } else if (sStatic.mBluetoothA2dpRoute != null) { - removeRoute(sStatic.mBluetoothA2dpRoute); + removeRouteStatic(sStatic.mBluetoothA2dpRoute); sStatic.mBluetoothA2dpRoute = null; } } @@ -205,16 +225,52 @@ public class MediaRouter { if (mBluetoothA2dpRoute != null) { if (mainType != AudioRoutesInfo.MAIN_SPEAKER && mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false); } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) && a2dpEnabled) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false); } } } - void updateActiveScan() { - if (hasActiveScanCallbackOfType(ROUTE_TYPE_LIVE_VIDEO)) { + void updateDiscoveryRequest() { + // What are we looking for today? + int routeTypes = 0; + int passiveRouteTypes = 0; + boolean activeScan = false; + boolean activeScanWifiDisplay = false; + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN + | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) { + // Discovery explicitly requested. + routeTypes |= cbi.type; + } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) { + // Discovery only passively requested. + passiveRouteTypes |= cbi.type; + } else { + // Legacy case since applications don't specify the discovery flag. + // Unfortunately we just have to assume they always need discovery + // whenever they have a callback registered. + routeTypes |= cbi.type; + } + if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { + activeScan = true; + if ((cbi.type & (ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_REMOTE_DISPLAY)) != 0) { + activeScanWifiDisplay = true; + } + } + } + if (routeTypes != 0 || activeScan) { + // If someone else requests discovery then enable the passive listeners. + // This is used by the MediaRouteButton and MediaRouteActionProvider since + // they don't receive lifecycle callbacks from the Activity. + routeTypes |= passiveRouteTypes; + } + + // Update wifi display scanning. + if (activeScanWifiDisplay) { if (!mActivelyScanningWifiDisplays) { mActivelyScanningWifiDisplays = true; mHandler.post(mScanWifiDisplays); @@ -225,18 +281,14 @@ public class MediaRouter { mHandler.removeCallbacks(mScanWifiDisplays); } } - } - private boolean hasActiveScanCallbackOfType(int type) { - final int count = mCallbacks.size(); - for (int i = 0; i < count; i++) { - CallbackInfo cbi = mCallbacks.get(i); - if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0 - && (cbi.type & type) != 0) { - return true; - } + // Tell the media router service all about it. + if (routeTypes != mDiscoveryRequestRouteTypes + || activeScan != mDiscoverRequestActiveScan) { + mDiscoveryRequestRouteTypes = routeTypes; + mDiscoverRequestActiveScan = activeScan; + publishClientDiscoveryRequest(); } - return false; } @Override @@ -271,6 +323,270 @@ public class MediaRouter { } } } + + void setSelectedRoute(RouteInfo info, boolean explicit) { + // Must be non-reentrant. + mSelectedRoute = info; + publishClientSelectedRoute(explicit); + } + + void rebindAsUser(int userId) { + if (mCurrentUserId != userId || userId < 0 || mClient == null) { + if (mClient != null) { + try { + mMediaRouterService.unregisterClient(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to unregister media router client.", ex); + } + mClient = null; + } + + mCurrentUserId = userId; + + try { + Client client = new Client(); + mMediaRouterService.registerClientAsUser(client, + mAppContext.getPackageName(), userId); + mClient = client; + } catch (RemoteException ex) { + Log.e(TAG, "Unable to register media router client.", ex); + } + + publishClientDiscoveryRequest(); + publishClientSelectedRoute(false); + updateClientState(); + } + } + + void publishClientDiscoveryRequest() { + if (mClient != null) { + try { + mMediaRouterService.setDiscoveryRequest(mClient, + mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to publish media router client discovery request.", ex); + } + } + } + + void publishClientSelectedRoute(boolean explicit) { + if (mClient != null) { + try { + mMediaRouterService.setSelectedRoute(mClient, + mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null, + explicit); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to publish media router client selected route.", ex); + } + } + } + + void updateClientState() { + // Update the client state. + mClientState = null; + if (mClient != null) { + try { + mClientState = mMediaRouterService.getState(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to retrieve media router client state.", ex); + } + } + final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes = + mClientState != null ? mClientState.routes : null; + final String globallySelectedRouteId = mClientState != null ? + mClientState.globallySelectedRouteId : null; + + // Add or update routes. + final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0; + for (int i = 0; i < globalRouteCount; i++) { + final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i); + RouteInfo route = findGlobalRoute(globalRoute.id); + if (route == null) { + route = makeGlobalRoute(globalRoute); + addRouteStatic(route); + } else { + updateGlobalRoute(route, globalRoute); + } + } + + // Synchronize state with the globally selected route. + if (globallySelectedRouteId != null) { + final RouteInfo route = findGlobalRoute(globallySelectedRouteId); + if (route == null) { + Log.w(TAG, "Could not find new globally selected route: " + + globallySelectedRouteId); + } else if (route != mSelectedRoute) { + if (DEBUG) { + Log.d(TAG, "Selecting new globally selected route: " + route); + } + selectRouteStatic(route.mSupportedTypes, route, false); + } + } else if (mSelectedRoute != null && mSelectedRoute.mGlobalRouteId != null) { + if (DEBUG) { + Log.d(TAG, "Unselecting previous globally selected route: " + mSelectedRoute); + } + selectDefaultRouteStatic(); + } + + // Remove defunct routes. + outer: for (int i = mRoutes.size(); i-- > 0; ) { + final RouteInfo route = mRoutes.get(i); + final String globalRouteId = route.mGlobalRouteId; + if (globalRouteId != null) { + for (int j = 0; j < globalRouteCount; j++) { + MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j); + if (globalRouteId.equals(globalRoute.id)) { + continue outer; // found + } + } + // not found + removeRouteStatic(route); + } + } + } + + void requestSetVolume(RouteInfo route, int volume) { + if (route.mGlobalRouteId != null && mClient != null) { + try { + mMediaRouterService.requestSetVolume(mClient, + route.mGlobalRouteId, volume); + } catch (RemoteException ex) { + Log.w(TAG, "Unable to request volume change.", ex); + } + } + } + + void requestUpdateVolume(RouteInfo route, int direction) { + if (route.mGlobalRouteId != null && mClient != null) { + try { + mMediaRouterService.requestUpdateVolume(mClient, + route.mGlobalRouteId, direction); + } catch (RemoteException ex) { + Log.w(TAG, "Unable to request volume change.", ex); + } + } + } + + RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) { + RouteInfo route = new RouteInfo(sStatic.mSystemCategory); + route.mGlobalRouteId = globalRoute.id; + route.mName = globalRoute.name; + route.mDescription = globalRoute.description; + route.mSupportedTypes = globalRoute.supportedTypes; + route.mEnabled = globalRoute.enabled; + route.setStatusCode(globalRoute.statusCode); + route.mPlaybackType = globalRoute.playbackType; + route.mPlaybackStream = globalRoute.playbackStream; + route.mVolume = globalRoute.volume; + route.mVolumeMax = globalRoute.volumeMax; + route.mVolumeHandling = globalRoute.volumeHandling; + route.mPresentationDisplay = getDisplayForGlobalRoute(globalRoute); + return route; + } + + void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) { + boolean changed = false; + boolean volumeChanged = false; + boolean presentationDisplayChanged = false; + + if (!Objects.equal(route.mName, globalRoute.name)) { + route.mName = globalRoute.name; + changed = true; + } + if (!Objects.equal(route.mDescription, globalRoute.description)) { + route.mDescription = globalRoute.description; + changed = true; + } + if (route.mSupportedTypes != globalRoute.supportedTypes) { + route.mSupportedTypes = globalRoute.supportedTypes; + changed = true; + } + if (route.mEnabled != globalRoute.enabled) { + route.mEnabled = globalRoute.enabled; + changed = true; + } + if (route.mStatusCode != globalRoute.statusCode) { + route.setStatusCode(globalRoute.statusCode); + changed = true; + } + if (route.mPlaybackType != globalRoute.playbackType) { + route.mPlaybackType = globalRoute.playbackType; + changed = true; + } + if (route.mPlaybackStream != globalRoute.playbackStream) { + route.mPlaybackStream = globalRoute.playbackStream; + changed = true; + } + if (route.mVolume != globalRoute.volume) { + route.mVolume = globalRoute.volume; + changed = true; + volumeChanged = true; + } + if (route.mVolumeMax != globalRoute.volumeMax) { + route.mVolumeMax = globalRoute.volumeMax; + changed = true; + volumeChanged = true; + } + if (route.mVolumeHandling != globalRoute.volumeHandling) { + route.mVolumeHandling = globalRoute.volumeHandling; + changed = true; + volumeChanged = true; + } + final Display presentationDisplay = getDisplayForGlobalRoute(globalRoute); + if (route.mPresentationDisplay != presentationDisplay) { + route.mPresentationDisplay = presentationDisplay; + changed = true; + presentationDisplayChanged = true; + } + + if (changed) { + dispatchRouteChanged(route); + } + if (volumeChanged) { + dispatchRouteVolumeChanged(route); + } + if (presentationDisplayChanged) { + dispatchRoutePresentationDisplayChanged(route); + } + } + + Display getDisplayForGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) { + // Ensure that the specified display is valid for presentations. + // This check will normally disallow the default display unless it was configured + // as a presentation display for some reason. + if (globalRoute.presentationDisplayId >= 0) { + Display display = mDisplayService.getDisplay(globalRoute.presentationDisplayId); + if (display != null && display.isPublicPresentation()) { + return display; + } + } + return null; + } + + RouteInfo findGlobalRoute(String globalRouteId) { + final int count = mRoutes.size(); + for (int i = 0; i < count; i++) { + final RouteInfo route = mRoutes.get(i); + if (globalRouteId.equals(route.mGlobalRouteId)) { + return route; + } + } + return null; + } + + final class Client extends IMediaRouterClient.Stub { + @Override + public void onStateChanged() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (Client.this == mClient) { + updateClientState(); + } + } + }); + } + } } static Static sStatic; @@ -285,7 +601,7 @@ public class MediaRouter { * <p>Once initiated this routing is transparent to the application. All audio * played on the media stream will be routed to the selected destination.</p> */ - public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0; /** * Route type flag for live video. @@ -302,7 +618,13 @@ public class MediaRouter { * @see RouteInfo#getPresentationDisplay() * @see android.app.Presentation */ - public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1; + + /** + * Temporary interop constant to identify remote displays. + * @hide To be removed when media router API is updated. + */ + public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2; /** * Route type flag for application-specific usage. @@ -312,7 +634,10 @@ public class MediaRouter { * is expected to interpret the meaning of these events and perform the requested * routing tasks.</p> */ - public static final int ROUTE_TYPE_USER = 0x00800000; + public static final int ROUTE_TYPE_USER = 1 << 23; + + static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO + | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER; /** * Flag for {@link #addCallback}: Actively scan for routes while this callback @@ -336,11 +661,27 @@ public class MediaRouter { * Flag for {@link #addCallback}: Do not filter route events. * <p> * When this flag is specified, the callback will be invoked for event that affect any - * route event if they do not match the callback's associated media route selector. + * route even if they do not match the callback's filter. * </p> */ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1; + /** + * Explicitly requests discovery. + * + * @hide Future API ported from support library. Revisit this later. + */ + public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2; + + /** + * Requests that discovery be performed but only if there is some other active + * callback already registered. + * + * @hide Compatibility workaround for the fact that applications do not currently + * request discovery explicitly (except when using the support library API). + */ + public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3; + // Maps application contexts static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); @@ -352,6 +693,9 @@ public class MediaRouter { if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) { result.append("ROUTE_TYPE_LIVE_VIDEO "); } + if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) { + result.append("ROUTE_TYPE_REMOTE_DISPLAY "); + } if ((types & ROUTE_TYPE_USER) != 0) { result.append("ROUTE_TYPE_USER "); } @@ -453,9 +797,7 @@ public class MediaRouter { info = new CallbackInfo(cb, types, flags, this); sStatic.mCallbacks.add(info); } - if ((info.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { - sStatic.updateActiveScan(); - } + sStatic.updateDiscoveryRequest(); } /** @@ -466,10 +808,8 @@ public class MediaRouter { public void removeCallback(Callback cb) { int index = findCallbackInfo(cb); if (index >= 0) { - CallbackInfo info = sStatic.mCallbacks.remove(index); - if ((info.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { - sStatic.updateActiveScan(); - } + sStatic.mCallbacks.remove(index); + sStatic.updateDiscoveryRequest(); } else { Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); } @@ -499,17 +839,17 @@ public class MediaRouter { * @param route Route to select */ public void selectRoute(int types, RouteInfo route) { - selectRouteStatic(types, route); + selectRouteStatic(types, route, true); } - + /** * @hide internal use */ - public void selectRouteInt(int types, RouteInfo route) { - selectRouteStatic(types, route); + public void selectRouteInt(int types, RouteInfo route, boolean explicit) { + selectRouteStatic(types, route, explicit); } - static void selectRouteStatic(int types, RouteInfo route) { + static void selectRouteStatic(int types, RouteInfo route, boolean explicit) { final RouteInfo oldRoute = sStatic.mSelectedRoute; if (oldRoute == route) return; if ((route.getSupportedTypes() & types) == 0) { @@ -541,15 +881,26 @@ public class MediaRouter { } } + sStatic.setSelectedRoute(route, explicit); + if (oldRoute != null) { dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute); } - sStatic.mSelectedRoute = route; if (route != null) { dispatchRouteSelected(types & route.getSupportedTypes(), route); } } + static void selectDefaultRouteStatic() { + // TODO: Be smarter about the route types here; this selects for all valid. + if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute + && sStatic.mBluetoothA2dpRoute != null) { + selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false); + } else { + selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false); + } + } + /** * Compare the device address of a display and a route. * Nulls/no device address will match another null/no address. @@ -612,7 +963,7 @@ public class MediaRouter { * @see #addUserRoute(UserRouteInfo) */ public void removeUserRoute(UserRouteInfo info) { - removeRoute(info); + removeRouteStatic(info); } /** @@ -626,7 +977,7 @@ public class MediaRouter { // TODO Right now, RouteGroups only ever contain user routes. // The code below will need to change if this assumption does. if (info instanceof UserRouteInfo || info instanceof RouteGroup) { - removeRouteAt(i); + removeRouteStatic(info); i--; } } @@ -636,10 +987,10 @@ public class MediaRouter { * @hide internal use only */ public void removeRouteInt(RouteInfo info) { - removeRoute(info); + removeRouteStatic(info); } - static void removeRoute(RouteInfo info) { + static void removeRouteStatic(RouteInfo info) { if (sStatic.mRoutes.remove(info)) { final RouteCategory removingCat = info.getCategory(); final int count = sStatic.mRoutes.size(); @@ -653,40 +1004,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. - if (info != sStatic.mBluetoothA2dpRoute && sStatic.mBluetoothA2dpRoute != null) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, - sStatic.mBluetoothA2dpRoute); - } else { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, - sStatic.mDefaultAudioVideo); - } - } - if (!found) { - sStatic.mCategories.remove(removingCat); - } - dispatchRouteRemoved(info); - } - } - - void removeRouteAt(int routeIndex) { - if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { - final RouteInfo info = sStatic.mRoutes.remove(routeIndex); - final RouteCategory removingCat = info.getCategory(); - final int count = sStatic.mRoutes.size(); - boolean found = false; - for (int i = 0; i < count; i++) { - final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); - if (removingCat == cat) { - found = true; - break; - } - } - 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_LIVE_VIDEO | ROUTE_TYPE_USER, - sStatic.mDefaultAudioVideo); + selectDefaultRouteStatic(); } if (!found) { sStatic.mCategories.remove(removingCat); @@ -752,7 +1070,7 @@ public class MediaRouter { * * @see #addUserRoute(UserRouteInfo) * @see #removeUserRoute(UserRouteInfo) - * @see #createRouteCategory(CharSequence) + * @see #createRouteCategory(CharSequence, boolean) */ public UserRouteInfo createUserRoute(RouteCategory category) { return new UserRouteInfo(category); @@ -780,6 +1098,23 @@ public class MediaRouter { return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); } + /** + * Rebinds the media router to handle routes that belong to the specified user. + * Requires the interact across users permission to access the routes of another user. + * <p> + * This method is a complete hack to work around the singleton nature of the + * media router when running inside of singleton processes like QuickSettings. + * This mechanism should be burned to the ground when MediaRouter is redesigned. + * Ideally the current user would be pulled from the Context but we need to break + * down MediaRouter.Static before we can get there. + * </p> + * + * @hide + */ + public void rebindAsUser(int userId) { + sStatic.rebindAsUser(userId); + } + static void updateRoute(final RouteInfo info) { dispatchRouteChanged(info); } @@ -906,7 +1241,7 @@ public class MediaRouter { updateWifiDisplayRoute(route, d, newStatus); } if (d.equals(activeDisplay)) { - selectRouteStatic(route.getSupportedTypes(), route); + selectRouteStatic(route.getSupportedTypes(), route, false); // Don't scan if we're already connected to a wifi display, // the scanning process can cause a hiccup with some configurations. @@ -919,7 +1254,7 @@ public class MediaRouter { if (d.isRemembered()) { final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); if (newDisplay == null || !newDisplay.isRemembered()) { - removeRoute(findWifiDisplayRoute(d)); + removeRouteStatic(findWifiDisplayRoute(d)); } } } @@ -932,8 +1267,7 @@ public class MediaRouter { } static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) { - int newStatus = RouteInfo.STATUS_NONE; - + int newStatus; if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) { newStatus = RouteInfo.STATUS_SCANNING; } else if (d.isAvailable()) { @@ -947,7 +1281,7 @@ public class MediaRouter { final int activeState = wfdStatus.getActiveDisplayState(); switch (activeState) { case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: - newStatus = RouteInfo.STATUS_NONE; + newStatus = RouteInfo.STATUS_CONNECTED; break; case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: newStatus = RouteInfo.STATUS_CONNECTING; @@ -968,7 +1302,8 @@ public class MediaRouter { static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) { final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); newRoute.mDeviceAddress = display.getDeviceAddress(); - newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; + newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO + | ROUTE_TYPE_REMOTE_DISPLAY; newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; @@ -1004,8 +1339,7 @@ public class MediaRouter { if (!enabled && route == sStatic.mSelectedRoute) { // Oops, no longer available. Reselect the default. - final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; - selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); + selectDefaultRouteStatic(); } } @@ -1075,6 +1409,10 @@ public class MediaRouter { String mDeviceAddress; boolean mEnabled = true; + // An id by which the route is known to the media router service. + // Null if this route only exists as an artifact within this process. + String mGlobalRouteId; + // A predetermined connection status that can override mStatus private int mStatusCode; @@ -1084,19 +1422,20 @@ public class MediaRouter { /** @hide */ public static final int STATUS_AVAILABLE = 3; /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; /** @hide */ public static final int STATUS_IN_USE = 5; + /** @hide */ public static final int STATUS_CONNECTED = 6; private Object mTag; /** * The default playback type, "local", indicating the presentation of the media is happening * on the same device (e.g. a phone, a tablet) as where it is controlled from. - * @see #setPlaybackType(int) + * @see #getPlaybackType() */ public final static int PLAYBACK_TYPE_LOCAL = 0; /** * A playback type indicating the presentation of the media is happening on * a different device (i.e. the remote device) than where it is controlled from. - * @see #setPlaybackType(int) + * @see #getPlaybackType() */ public final static int PLAYBACK_TYPE_REMOTE = 1; /** @@ -1104,12 +1443,13 @@ public class MediaRouter { * controlled from this object. An example of fixed playback volume is a remote player, * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather * than attenuate at the source. - * @see #setVolumeHandling(int) + * @see #getVolumeHandling() */ public final static int PLAYBACK_VOLUME_FIXED = 0; /** * Playback information indicating the playback volume is variable and can be controlled * from this object. + * @see #getVolumeHandling() */ public final static int PLAYBACK_VOLUME_VARIABLE = 1; @@ -1181,7 +1521,7 @@ public class MediaRouter { boolean setStatusCode(int statusCode) { if (statusCode != mStatusCode) { mStatusCode = statusCode; - int resId = 0; + int resId; switch (statusCode) { case STATUS_SCANNING: resId = com.android.internal.R.string.media_route_status_scanning; @@ -1198,6 +1538,11 @@ public class MediaRouter { case STATUS_IN_USE: resId = com.android.internal.R.string.media_route_status_in_use; break; + case STATUS_CONNECTED: + case STATUS_NONE: + default: + resId = 0; + break; } mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; return true; @@ -1317,9 +1662,7 @@ public class MediaRouter { Log.e(TAG, "Error setting local stream volume", e); } } else { - Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + - "Non-local volume playback on system route? " + - "Could not request volume change."); + sStatic.requestSetVolume(this, volume); } } @@ -1338,9 +1681,7 @@ public class MediaRouter { Log.e(TAG, "Error setting local stream volume", e); } } else { - Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + - "Non-local volume playback on system route? " + - "Could not request volume change."); + sStatic.requestUpdateVolume(this, direction); } } @@ -1418,7 +1759,19 @@ public class MediaRouter { * @return True if this route is in the process of connecting. */ public boolean isConnecting() { - return mStatusCode == STATUS_CONNECTING; + // If the route is selected and its status appears to be between states + // then report it as connecting even though it has not yet had a chance + // to move into the CONNECTING state. Note that routes in the NONE state + // are assumed to not require an explicit connection lifecycle. + if (this == sStatic.mSelectedRoute) { + switch (mStatusCode) { + case STATUS_AVAILABLE: + case STATUS_SCANNING: + case STATUS_CONNECTING: + return true; + } + } + return false; } void setStatusInt(CharSequence status) { @@ -1432,6 +1785,7 @@ public class MediaRouter { } final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { + @Override public void dispatchRemoteVolumeUpdate(final int direction, final int value) { sStatic.mHandler.post(new Runnable() { @Override @@ -1460,7 +1814,7 @@ public class MediaRouter { ", status=" + getStatus() + ", category=" + getCategory() + ", supportedTypes=" + supportedTypes + - ", presentationDisplay=" + mPresentationDisplay + "}"; + ", presentationDisplay=" + mPresentationDisplay + " }"; } } @@ -1716,6 +2070,7 @@ public class MediaRouter { mVolumeHandling = PLAYBACK_VOLUME_FIXED; } + @Override CharSequence getName(Resources res) { if (mUpdateName) updateName(); return super.getName(res); @@ -1916,7 +2271,7 @@ public class MediaRouter { final int count = mRoutes.size(); if (count == 0) { // Don't keep empty groups in the router. - MediaRouter.removeRoute(this); + MediaRouter.removeRouteStatic(this); return; } @@ -2071,6 +2426,7 @@ public class MediaRouter { return mIsSystem; } + @Override public String toString() { return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + " groupable=" + mGroupable + " }"; diff --git a/media/java/android/media/MediaRouterClientState.aidl b/media/java/android/media/MediaRouterClientState.aidl new file mode 100644 index 0000000..70077119 --- /dev/null +++ b/media/java/android/media/MediaRouterClientState.aidl @@ -0,0 +1,18 @@ +/* Copyright 2013, 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 android.media; + +parcelable MediaRouterClientState; diff --git a/media/java/android/media/MediaRouterClientState.java b/media/java/android/media/MediaRouterClientState.java new file mode 100644 index 0000000..0847503 --- /dev/null +++ b/media/java/android/media/MediaRouterClientState.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2013 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 android.media; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; + +/** + * Information available from MediaRouterService about the state perceived by + * a particular client and the routes that are available to it. + * + * Clients must not modify the contents of this object. + * @hide + */ +public final class MediaRouterClientState implements Parcelable { + /** + * A list of all known routes. + */ + public final ArrayList<RouteInfo> routes; + + /** + * The id of the current globally selected route, or null if none. + * Globally selected routes override any other route selections that applications + * may have made. Used for remote displays. + */ + public String globallySelectedRouteId; + + public MediaRouterClientState() { + routes = new ArrayList<RouteInfo>(); + } + + MediaRouterClientState(Parcel src) { + routes = src.createTypedArrayList(RouteInfo.CREATOR); + globallySelectedRouteId = src.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(routes); + dest.writeString(globallySelectedRouteId); + } + + public static final Parcelable.Creator<MediaRouterClientState> CREATOR = + new Parcelable.Creator<MediaRouterClientState>() { + @Override + public MediaRouterClientState createFromParcel(Parcel in) { + return new MediaRouterClientState(in); + } + + @Override + public MediaRouterClientState[] newArray(int size) { + return new MediaRouterClientState[size]; + } + }; + + public static final class RouteInfo implements Parcelable { + public String id; + public String name; + public String description; + public int supportedTypes; + public boolean enabled; + public int statusCode; + public int playbackType; + public int playbackStream; + public int volume; + public int volumeMax; + public int volumeHandling; + public int presentationDisplayId; + + public RouteInfo(String id) { + this.id = id; + enabled = true; + statusCode = MediaRouter.RouteInfo.STATUS_NONE; + playbackType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; + playbackStream = -1; + volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; + presentationDisplayId = -1; + } + + public RouteInfo(RouteInfo other) { + id = other.id; + name = other.name; + description = other.description; + supportedTypes = other.supportedTypes; + enabled = other.enabled; + statusCode = other.statusCode; + playbackType = other.playbackType; + playbackStream = other.playbackStream; + volume = other.volume; + volumeMax = other.volumeMax; + volumeHandling = other.volumeHandling; + presentationDisplayId = other.presentationDisplayId; + } + + RouteInfo(Parcel in) { + id = in.readString(); + name = in.readString(); + description = in.readString(); + supportedTypes = in.readInt(); + enabled = in.readInt() != 0; + statusCode = in.readInt(); + playbackType = in.readInt(); + playbackStream = in.readInt(); + volume = in.readInt(); + volumeMax = in.readInt(); + volumeHandling = in.readInt(); + presentationDisplayId = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(name); + dest.writeString(description); + dest.writeInt(supportedTypes); + dest.writeInt(enabled ? 1 : 0); + dest.writeInt(statusCode); + dest.writeInt(playbackType); + dest.writeInt(playbackStream); + dest.writeInt(volume); + dest.writeInt(volumeMax); + dest.writeInt(volumeHandling); + dest.writeInt(presentationDisplayId); + } + + @Override + public String toString() { + return "RouteInfo{ id=" + id + + ", name=" + name + + ", description=" + description + + ", supportedTypes=0x" + Integer.toHexString(supportedTypes) + + ", enabled=" + enabled + + ", statusCode=" + statusCode + + ", playbackType=" + playbackType + + ", playbackStream=" + playbackStream + + ", volume=" + volume + + ", volumeMax=" + volumeMax + + ", volumeHandling=" + volumeHandling + + ", presentationDisplayId=" + presentationDisplayId + + " }"; + } + + @SuppressWarnings("hiding") + public static final Parcelable.Creator<RouteInfo> CREATOR = + new Parcelable.Creator<RouteInfo>() { + @Override + public RouteInfo createFromParcel(Parcel in) { + return new RouteInfo(in); + } + + @Override + public RouteInfo[] newArray(int size) { + return new RouteInfo[size]; + } + }; + } +} |