From 67c41fddd8926f70774878e5af70a9cb81393641 Mon Sep 17 00:00:00 2001 From: Hyundo Moon Date: Fri, 17 Jan 2020 14:22:42 +0900 Subject: [PATCH] MediaRouter2: Add getDefaultController() The controller which can control the system routes (phone speaker, bluetooth, etc) is returned by this new method. This CL makes the default controller can know the current system route, and make SessionCallback#onSessionInfoChanged() be called when the current system route is changed. Note that selecting route (BT / speaker) via this controller is not yet supported by this CL. Bug: 147937954 Test: atest mediaroutertest Also, manually checked logs by connecting bluetooth device. Change-Id: I8c6254855f8121d1d2331959224fe7237affa1e2 --- .../android/media/IMediaRouterService.aidl | 1 + media/java/android/media/MediaRouter2.java | 56 ++++++++++++++++++- .../java/android/media/MediaRouter2Utils.java | 7 --- .../android/media/RoutingSessionInfo.java | 26 +++++++++ .../mediaroutertest/MediaRouter2Test.java | 8 +++ .../MediaRouterManagerTest.java | 2 +- .../server/media/BluetoothRouteProvider.java | 12 +++- .../server/media/MediaRouter2ServiceImpl.java | 45 +++++++++++++++ .../server/media/MediaRouterService.java | 6 ++ .../media/SystemMediaRoute2Provider.java | 53 +++++++++++++++++- 10 files changed, 203 insertions(+), 13 deletions(-) diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 281e7c6b6f683..dac0fba876be2 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -46,6 +46,7 @@ interface IMediaRouterService { // Methods for media router 2 List getSystemRoutes(); + RoutingSessionInfo getSystemSessionInfo(); void registerClient2(IMediaRouter2Client client, String packageName); void unregisterClient2(IMediaRouter2Client client); void sendControlRequest(IMediaRouter2Client client, in MediaRoute2Info route, diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 51d08ec96e6f5..a6198842abe85 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -76,6 +76,8 @@ public class MediaRouter2 { @GuardedBy("sRouterLock") final Map mRoutes = new HashMap<>(); + final RoutingController mDefaultController; + @GuardedBy("sRouterLock") private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; @@ -116,19 +118,26 @@ public class MediaRouter2 { mHandler = new Handler(Looper.getMainLooper()); List currentSystemRoutes = null; + RoutingSessionInfo currentSystemSessionInfo = null; try { currentSystemRoutes = mMediaRouterService.getSystemRoutes(); + currentSystemSessionInfo = mMediaRouterService.getSystemSessionInfo(); } catch (RemoteException ex) { - Log.e(TAG, "Unable to get current currentSystemRoutes", ex); + Log.e(TAG, "Unable to get current system's routes / session info", ex); } if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) { throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong."); } + if (currentSystemSessionInfo == null) { + throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong."); + } + for (MediaRoute2Info route : currentSystemRoutes) { mRoutes.put(route.getId(), route); } + mDefaultController = new DefaultRoutingController(currentSystemSessionInfo); } /** @@ -340,6 +349,22 @@ public class MediaRouter2 { } } + /** + * Gets a {@link RoutingController} which can control the routes provided by system. + * e.g. Phone speaker, wired headset, Bluetooth, etc. + *

+ * Note: The default controller can't be released. Calling {@link RoutingController#release()} + * will be no-op. + *

+ * This method will always return the same instance. + * + * @hide + */ + @NonNull + public RoutingController getDefaultController() { + return mDefaultController; + } + /** * Sends a media control request to be performed asynchronously by the route's destination. * @@ -526,6 +551,15 @@ public class MediaRouter2 { return; } + if (sessionInfo.isSystemSession()) { + // The session info is sent from SystemMediaRoute2Provider. + RoutingController defaultController = getDefaultController(); + RoutingSessionInfo oldInfo = defaultController.getRoutingSessionInfo(); + defaultController.setRoutingSessionInfo(sessionInfo); + notifySessionInfoChanged(defaultController, oldInfo, sessionInfo); + return; + } + RoutingController matchingController; synchronized (sRouterLock) { matchingController = mRoutingControllers.get(sessionInfo.getId()); @@ -766,7 +800,7 @@ public class MediaRouter2 { * * @hide */ - public final class RoutingController { + public class RoutingController { private final Object mControllerLock = new Object(); @GuardedBy("mControllerLock") @@ -1074,7 +1108,6 @@ public class MediaRouter2 { // TODO: This method uses two locks (mLock outside, sLock inside). // Check if there is any possiblity of deadlock. private List getRoutesWithIdsLocked(List routeIds) { - List routes = new ArrayList<>(); synchronized (sRouterLock) { // TODO: Maybe able to change using Collection.stream()? @@ -1089,6 +1122,23 @@ public class MediaRouter2 { } } + class DefaultRoutingController extends RoutingController { + DefaultRoutingController(@NonNull RoutingSessionInfo sessionInfo) { + super(sessionInfo); + } + + @Override + public void release() { + // Do nothing. DefaultRoutingController will never be released + } + + @Override + public boolean isReleased() { + // DefaultRoutingController will never be released + return false; + } + } + final class RouteCallbackRecord { public final Executor mExecutor; public final RouteCallback mRouteCallback; diff --git a/media/java/android/media/MediaRouter2Utils.java b/media/java/android/media/MediaRouter2Utils.java index 49045828dbe87..c15972dcff2ec 100644 --- a/media/java/android/media/MediaRouter2Utils.java +++ b/media/java/android/media/MediaRouter2Utils.java @@ -29,9 +29,6 @@ public class MediaRouter2Utils { static final String TAG = "MR2Utils"; static final String SEPARATOR = ":"; - /** - * @hide - */ @NonNull public static String toUniqueId(@NonNull String providerId, @NonNull String id) { if (TextUtils.isEmpty(providerId)) { @@ -49,8 +46,6 @@ public class MediaRouter2Utils { /** * Gets provider ID from unique ID. * If the corresponding provider ID could not be generated, it will return null. - * - * @hide */ @Nullable public static String getProviderId(@NonNull String uniqueId) { @@ -75,8 +70,6 @@ public class MediaRouter2Utils { /** * Gets the original ID (i.e. non-unique route/session ID) from unique ID. * If the corresponding ID could not be generated, it will return null. - * - * @hide */ @Nullable public static String getOriginalId(@NonNull String uniqueId) { diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 228addebe0296..cb996f493eb44 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -59,6 +59,7 @@ public final class RoutingSessionInfo implements Parcelable { final List mTransferrableRoutes; @Nullable final Bundle mControlHints; + final boolean mIsSystemSession; RoutingSessionInfo(@NonNull Builder builder) { Objects.requireNonNull(builder, "builder must not be null."); @@ -78,6 +79,7 @@ public final class RoutingSessionInfo implements Parcelable { convertToUniqueRouteIds(builder.mTransferrableRoutes)); mControlHints = builder.mControlHints; + mIsSystemSession = builder.mIsSystemSession; } RoutingSessionInfo(@NonNull Parcel src) { @@ -93,6 +95,7 @@ public final class RoutingSessionInfo implements Parcelable { mTransferrableRoutes = ensureList(src.createStringArrayList()); mControlHints = src.readBundle(); + mIsSystemSession = src.readBoolean(); } private static String ensureString(String str) { @@ -193,6 +196,15 @@ public final class RoutingSessionInfo implements Parcelable { return mControlHints; } + /** + * Gets whether this session is in system media route provider. + * @hide + */ + @Nullable + public boolean isSystemSession() { + return mIsSystemSession; + } + @Override public int describeContents() { return 0; @@ -208,6 +220,7 @@ public final class RoutingSessionInfo implements Parcelable { dest.writeStringList(mDeselectableRoutes); dest.writeStringList(mTransferrableRoutes); dest.writeBundle(mControlHints); + dest.writeBoolean(mIsSystemSession); } @Override @@ -278,6 +291,7 @@ public final class RoutingSessionInfo implements Parcelable { * Builder class for {@link RoutingSessionInfo}. */ public static final class Builder { + // TODO: Reorder these (important ones first) final String mId; final String mClientPackageName; String mProviderId; @@ -286,6 +300,7 @@ public final class RoutingSessionInfo implements Parcelable { final List mDeselectableRoutes; final List mTransferrableRoutes; Bundle mControlHints; + boolean mIsSystemSession; /** * Constructor for builder to create {@link RoutingSessionInfo}. @@ -333,6 +348,7 @@ public final class RoutingSessionInfo implements Parcelable { mTransferrableRoutes = new ArrayList<>(sessionInfo.mTransferrableRoutes); mControlHints = sessionInfo.mControlHints; + mIsSystemSession = sessionInfo.mIsSystemSession; } /** @@ -490,6 +506,16 @@ public final class RoutingSessionInfo implements Parcelable { return this; } + /** + * Sets whether this session is in system media route provider. + * @hide + */ + @NonNull + public Builder setSystemSession(boolean isSystemSession) { + mIsSystemSession = isSystemSession; + return this; + } + /** * Builds a routing session info. * diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java index 32d03db6f7eea..a9fcf7703cfc3 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java @@ -692,6 +692,14 @@ public class MediaRouter2Test { } } + // TODO: Consider adding tests with bluetooth connection/disconnection. + @Test + public void testGetDefaultController() { + final RoutingController defaultController = mRouter2.getDefaultController(); + assertNotNull(defaultController); + assertFalse(defaultController.isReleased()); + } + // Helper for getting routes easily static Map createRouteMap(List routes) { Map routeMap = new HashMap<>(); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java index cba8452fe9c2d..39313eec6bf85 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java @@ -227,7 +227,7 @@ public class MediaRouterManagerTest { mManager.selectRoute(mPackageName, routeToSelect); assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - assertEquals(1, mManager.getActiveSessions().size()); + assertEquals(2, mManager.getActiveSessions().size()); } @Test diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index b67335ab82bc8..9f42c3625a51a 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -17,6 +17,7 @@ package com.android.server.media; import android.annotation.NonNull; +import android.annotation.Nullable; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -58,6 +59,7 @@ class BluetoothRouteProvider { private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver(); private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener(); + // TODO: The mActiveDevice should be set when BluetoothRouteProvider is created. private BluetoothDevice mActiveDevice = null; static synchronized BluetoothRouteProvider getInstance(@NonNull Context context, @@ -125,6 +127,14 @@ class BluetoothRouteProvider { return routes; } + @Nullable String getActiveDeviceAddress() { + BluetoothDevice device = mActiveDevice; + if (device == null) { + return null; + } + return device.getAddress(); + } + private void notifyBluetoothRoutesUpdated() { if (mListener != null) { mListener.onBluetoothRoutesUpdated(getBluetoothRoutes()); @@ -281,8 +291,8 @@ class BluetoothRouteProvider { setRouteConnectionStateForDevice(device, MediaRoute2Info.CONNECTION_STATE_CONNECTED); } - notifyBluetoothRoutesUpdated(); mActiveDevice = device; + notifyBluetoothRoutesUpdated(); } break; case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 2167530170102..f9169ee69d418 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -112,6 +112,30 @@ class MediaRouter2ServiceImpl { } } + @NonNull + public RoutingSessionInfo getSystemSessionInfo() { + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); + + final long token = Binder.clearCallingIdentity(); + try { + RoutingSessionInfo systemSessionInfo = null; + synchronized (mLock) { + UserRecord userRecord = getOrCreateUserRecordLocked(userId); + List sessionInfos = + userRecord.mHandler.mSystemProvider.getSessionInfos(); + if (sessionInfos != null && !sessionInfos.isEmpty()) { + systemSessionInfo = sessionInfos.get(0); + } else { + Slog.w(TAG, "System provider does not have any session info."); + } + } + return systemSessionInfo; + } finally { + Binder.restoreCallingIdentity(token); + } + } + public void registerClient(@NonNull IMediaRouter2Client client, @NonNull String packageName) { Objects.requireNonNull(client, "client must not be null"); @@ -1351,6 +1375,16 @@ class MediaRouter2ServiceImpl { List managers = getManagers(); notifySessionInfosChangedToManagers(managers); + // For system provider, notify all clients. + if (provider == mSystemProvider) { + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + notifySessionInfoChangedToClients(getClients(), sessionInfo); + return; + } + Client2Record client2Record = mSessionToClientMap.get( sessionInfo.getId()); if (client2Record == null) { @@ -1509,6 +1543,17 @@ class MediaRouter2ServiceImpl { } } + private void notifySessionInfoChangedToClients(List clients, + RoutingSessionInfo sessionInfo) { + for (IMediaRouter2Client client : clients) { + try { + client.notifySessionInfoChanged(sessionInfo); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify session info changed. Client probably died.", ex); + } + } + } + private void notifyRoutesToManager(IMediaRouter2Manager manager) { List routes = new ArrayList<>(); for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 5437fadf2d74d..e1d38039b9558 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -442,6 +442,12 @@ public final class MediaRouterService extends IMediaRouterService.Stub return mService2.getSystemRoutes(); } + // Binder call + @Override + public RoutingSessionInfo getSystemSessionInfo() { + return mService2.getSystemSessionInfo(); + } + // Binder call @Override public void registerClient2(IMediaRouter2Client client, String packageName) { diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 56c33fe339ea1..ebea6c3dc558b 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -26,17 +26,20 @@ import android.media.IAudioRoutesObserver; import android.media.IAudioService; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; +import android.media.RoutingSessionInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.text.TextUtils; import android.util.Log; import com.android.internal.R; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Provides routes for local playbacks such as phone speaker, wired headset, or Bluetooth speakers. @@ -46,7 +49,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; - static final String BLUETOOTH_ROUTE_ID = "BLUETOOTH_ROUTE"; + static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; // TODO: Move these to a proper place public static final String TYPE_LIVE_AUDIO = "android.media.intent.route.TYPE_LIVE_AUDIO"; @@ -92,6 +95,14 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mBtRouteProvider = BluetoothRouteProvider.getInstance(context, (routes) -> { mBluetoothRoutes = routes; publishRoutes(); + + boolean sessionInfoChanged; + synchronized (mLock) { + sessionInfoChanged = updateSessionInfosIfNeededLocked(); + } + if (sessionInfoChanged) { + notifySessionInfoUpdated(); + } }); initializeRoutes(); } @@ -172,6 +183,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } setProviderState(builder.build()); mHandler.post(() -> notifyProviderState()); + + // Note: No lock needed when initializing. + updateSessionInfosIfNeededLocked(); } void updateAudioRoutes(AudioRoutesInfo newRoutes) { @@ -202,6 +216,35 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { publishRoutes(); } + /** + * Updates the mSessionInfo. Returns true if the session info is changed. + */ + boolean updateSessionInfosIfNeededLocked() { + RoutingSessionInfo oldSessionInfo = mSessionInfos.isEmpty() ? null : mSessionInfos.get(0); + + RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder( + SYSTEM_SESSION_ID, "" /* clientPackageName */) + .setSystemSession(true); + String activeBtDeviceAddress = mBtRouteProvider.getActiveDeviceAddress(); + + RoutingSessionInfo newSessionInfo; + if (!TextUtils.isEmpty(activeBtDeviceAddress)) { + // Bluetooth route. Set the route ID with the device's address. + newSessionInfo = builder.addSelectedRoute(activeBtDeviceAddress).build(); + } else { + // Default device + newSessionInfo = builder.addSelectedRoute(mDefaultRoute.getId()).build(); + } + + if (Objects.equals(oldSessionInfo, newSessionInfo)) { + return false; + } else { + mSessionInfos.clear(); + mSessionInfos.add(newSessionInfo); + return true; + } + } + /** * The first route should be the currently selected system route. * For example, if there are two system routes (BT and device speaker), @@ -215,4 +258,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } setAndNotifyProviderState(builder.build()); } + + void notifySessionInfoUpdated() { + RoutingSessionInfo sessionInfo; + synchronized (mLock) { + sessionInfo = mSessionInfos.get(0); + } + mCallback.onSessionUpdated(this, sessionInfo); + } }