diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl index 66764c73ee5cd..d8fd1ff22c89d 100644 --- a/media/java/android/media/IMediaRoute2Provider.aidl +++ b/media/java/android/media/IMediaRoute2Provider.aidl @@ -24,7 +24,7 @@ import android.media.IMediaRoute2ProviderClient; */ oneway interface IMediaRoute2Provider { void setClient(IMediaRoute2ProviderClient client); - void selectRoute(String packageName, String id); + void requestSelectRoute(String packageName, String id, int seq); void unselectRoute(String packageName, String id); void notifyControlRequestSent(String id, in Intent request); void requestSetVolume(String id, int volume); diff --git a/media/java/android/media/IMediaRouter2Client.aidl b/media/java/android/media/IMediaRouter2Client.aidl index 72c33f9943499..b04af7d1d28d6 100644 --- a/media/java/android/media/IMediaRouter2Client.aidl +++ b/media/java/android/media/IMediaRouter2Client.aidl @@ -17,6 +17,7 @@ package android.media; import android.media.MediaRoute2Info; +import android.os.Bundle; /** * @hide @@ -26,4 +27,5 @@ oneway interface IMediaRouter2Client { void notifyRoutesAdded(in List routes); void notifyRoutesRemoved(in List routes); void notifyRoutesChanged(in List routes); + void notifyRouteSelected(in MediaRoute2Info route, int reason, in Bundle controlHints); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 7b7a34e5151f3..ced8615a3ee6e 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -54,7 +54,7 @@ interface IMediaRouterService { * @param client the client that changes it's selected route * @param route the route to be selected */ - void selectRoute2(IMediaRouter2Client client, in @nullable MediaRoute2Info route); + void requestSelectRoute2(IMediaRouter2Client client, in @nullable MediaRoute2Info route); void setControlCategories2(IMediaRouter2Client client, in List categories); void registerManager(IMediaRouter2Manager manager, String packageName); diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 5f5d200c6f5e9..386d2dc54a883 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -128,7 +128,9 @@ public abstract class MediaRoute2ProviderService extends Service { } @Override - public void selectRoute(String packageName, String id) { + public void requestSelectRoute(String packageName, String id, int seq) { + // TODO: When introducing MediaRoute2ProviderService#sendConnectionHints(), + // use the sequence number here properly. mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelectRoute, MediaRoute2ProviderService.this, packageName, id)); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 86fa9db32256f..74d26f0305297 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -30,6 +30,7 @@ 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.annotations.GuardedBy; @@ -103,6 +104,8 @@ public class MediaRouter2 { private MediaRoute2Info mSelectedRoute; @GuardedBy("sLock") + private MediaRoute2Info mSelectingRoute; + @GuardedBy("sLock") private Client mClient; final Handler mHandler; @@ -250,24 +253,28 @@ public class MediaRouter2 { } /** - * Selects the specified route. + * Request to select the specified route. When the route is selected, + * {@link Callback#onRouteSelected(MediaRoute2Info, int, Bundle)} will be called. * * @param route the route to select */ - //TODO: add a parameter for category (e.g. mirroring/casting) - public void selectRoute(@NonNull MediaRoute2Info route) { + public void requestSelectRoute(@NonNull MediaRoute2Info route) { Objects.requireNonNull(route, "route must not be null"); Client client; synchronized (sLock) { - mSelectedRoute = route; + if (mSelectingRoute == route) { + Log.w(TAG, "The route selection request is already sent."); + return; + } + mSelectingRoute = route; client = mClient; } if (client != null) { try { - mMediaRouterService.selectRoute2(client, route); + mMediaRouterService.requestSelectRoute2(client, route); } catch (RemoteException ex) { - Log.e(TAG, "Unable to select route.", ex); + Log.e(TAG, "Unable to request to select route.", ex); } } } @@ -443,6 +450,22 @@ public class MediaRouter2 { } } + void selectRouteOnHandler(MediaRoute2Info route, int reason, Bundle controlHints) { + synchronized (sLock) { + if (reason == SELECT_REASON_USER_SELECTED) { + if (mSelectingRoute == null + || !TextUtils.equals(mSelectingRoute.getUniqueId(), route.getUniqueId())) { + Log.w(TAG, "Ignoring invalid or outdated notifyRouteSelected call. " + + "selectingRoute=" + mSelectingRoute + " route=" + route); + return; + } + } + mSelectingRoute = null; + } + mSelectedRoute = route; + notifyRouteSelected(route, reason, controlHints); + } + private void refreshFilteredRoutes() { List filteredRoutes = new ArrayList<>(); @@ -475,12 +498,17 @@ public class MediaRouter2 { } } + private void notifyRouteSelected(MediaRoute2Info route, int reason, Bundle controlHints) { + for (CallbackRecord record: mCallbackRecords) { + record.mExecutor.execute( + () -> record.mCallback.onRouteSelected(route, reason, controlHints)); + } + } + /** * Interface for receiving events about media routing changes. */ public static class Callback { - //TODO: clean up these callbacks - /** * Called when routes are added. * @param routes the list of routes that have been added. It's never empty. @@ -505,20 +533,19 @@ public class MediaRouter2 { */ public void onRoutesChanged(@NonNull List routes) {} - // TODO: Make this callback be called when we add requestSelectRoute(). /** * Called when a route is selected. Exactly one route can be selected at a time. * @param route the selected route. * @param reason the reason why the route is selected. - * @param connectionHints An optional bundle of provider-specific arguments which may be - * used to control the selected route. Can be empty. + * @param controlHints An optional bundle of provider-specific arguments which may be + * used to control the selected route. Can be empty. * @see #SELECT_REASON_UNKNOWN * @see #SELECT_REASON_USER_SELECTED * @see #SELECT_REASON_FALLBACK * @see #getSelectedRoute() */ public void onRouteSelected(@NonNull MediaRoute2Info route, @SelectReason int reason, - @NonNull Bundle connectionHints) {} + @NonNull Bundle controlHints) {} } final class CallbackRecord { @@ -560,5 +587,12 @@ public class MediaRouter2 { mHandler.sendMessage(obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); } + + @Override + public void notifyRouteSelected(MediaRoute2Info route, int reason, + Bundle controlHints) { + mHandler.sendMessage(obtainMessage(MediaRouter2::selectRouteOnHandler, + MediaRouter2.this, route, reason, controlHints)); + } } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java index 626bf1cc361be..51a0df33bc654 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java @@ -84,9 +84,9 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { mCallback = callback; } - public void selectRoute(String packageName, String routeId) { + public void requestSelectRoute(String packageName, String routeId, int seq) { if (mConnectionReady) { - mActiveConnection.selectRoute(packageName, routeId); + mActiveConnection.requestSelectRoute(packageName, routeId, seq); updateBinding(); } } @@ -328,9 +328,9 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { mClient.dispose(); } - public void selectRoute(String packageName, String routeId) { + public void requestSelectRoute(String packageName, String routeId, int seq) { try { - mProvider.selectRoute(packageName, routeId); + mProvider.requestSelectRoute(packageName, routeId, seq); } catch (RemoteException ex) { Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex); } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 2e97f6a559d6f..adfb9cb0964ef 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -29,10 +29,13 @@ import android.media.IMediaRouter2Manager; import android.media.IMediaRouterClient; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; +import android.media.MediaRouter2; import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; @@ -60,6 +63,7 @@ import java.util.Set; class MediaRouter2ServiceImpl { private static final String TAG = "MR2ServiceImpl"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final long ROUTE_SELECTION_REQUEST_TIMEOUT_MS = 5000L; private final Context mContext; private final Object mLock = new Object(); @@ -72,6 +76,8 @@ class MediaRouter2ServiceImpl { private final ArrayMap mAllManagerRecords = new ArrayMap<>(); @GuardedBy("mLock") private int mCurrentUserId = -1; + @GuardedBy("mLock") + private int mSelectRouteRequestSequenceNumber = 0; MediaRouter2ServiceImpl(Context context) { mContext = context; @@ -189,12 +195,12 @@ class MediaRouter2ServiceImpl { } } - public void selectRoute2(@NonNull IMediaRouter2Client client, + public void requestSelectRoute2(@NonNull IMediaRouter2Client client, @Nullable MediaRoute2Info route) { final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - selectRoute2Locked(mAllClientRecords.get(client.asBinder()), route); + requestSelectRoute2Locked(mAllClientRecords.get(client.asBinder()), route); } } finally { Binder.restoreCallingIdentity(token); @@ -375,24 +381,38 @@ class MediaRouter2ServiceImpl { } } - private void selectRoute2Locked(ClientRecord clientRecord, MediaRoute2Info route) { + private void requestSelectRoute2Locked(ClientRecord clientRecord, MediaRoute2Info route) { if (clientRecord != null) { MediaRoute2Info oldRoute = clientRecord.mSelectedRoute; - clientRecord.mSelectedRoute = route; + clientRecord.mSelectingRoute = route; UserHandler handler = clientRecord.mUserRecord.mHandler; //TODO: Handle transfer instead of unselect and select if (oldRoute != null) { - handler.sendMessage( - obtainMessage(UserHandler::unselectRoute, handler, clientRecord, - oldRoute)); + handler.sendMessage(obtainMessage( + UserHandler::unselectRoute, handler, clientRecord.mPackageName, oldRoute)); } if (route != null) { - handler.sendMessage( - obtainMessage(UserHandler::selectRoute, handler, clientRecord, route)); + final int seq = mSelectRouteRequestSequenceNumber; + mSelectRouteRequestSequenceNumber++; + + handler.sendMessage(obtainMessage( + UserHandler::requestSelectRoute, handler, clientRecord.mPackageName, + route, seq)); + + // Remove all previous timeout messages + for (int previousSeq : clientRecord.mSelectRouteSequenceNumbers) { + clientRecord.mUserRecord.mHandler.removeMessages(previousSeq); + } + clientRecord.mSelectRouteSequenceNumbers.clear(); + + // When the request is not handled in timeout, set the client's route to default. + Message timeoutMsg = obtainMessage(UserHandler::handleRouteSelectionTimeout, + handler, clientRecord.mPackageName, route); + timeoutMsg.what = seq; // Make the message cancelable. + handler.sendMessageDelayed(timeoutMsg, ROUTE_SELECTION_REQUEST_TIMEOUT_MS); + clientRecord.mSelectRouteSequenceNumbers.add(seq); } - handler.sendMessage( - obtainMessage(UserHandler::updateClientUsage, handler, clientRecord)); } } @@ -473,6 +493,11 @@ class MediaRouter2ServiceImpl { userRecord.mHandler, manager)); for (ClientRecord clientRecord : userRecord.mClientRecords) { + // TODO: Do not use updateClientUsage since it updates all managers. + // Instead, Notify only to the manager that is currently being registered. + + // TODO: UserRecord <-> ClientRecord, why do they reference each other? + // How about removing mUserRecord from clientRecord? clientRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::updateClientUsage, clientRecord.mUserRecord.mHandler, clientRecord)); @@ -494,12 +519,13 @@ class MediaRouter2ServiceImpl { String packageName, MediaRoute2Info route) { ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder()); if (managerRecord != null) { - ClientRecord clientRecord = managerRecord.mUserRecord.findClientRecord(packageName); + ClientRecord clientRecord = + managerRecord.mUserRecord.findClientRecordLocked(packageName); if (clientRecord == null) { Slog.w(TAG, "Ignoring route selection for unknown client."); } if (clientRecord != null && managerRecord.mTrusted) { - selectRoute2Locked(clientRecord, route); + requestSelectRoute2Locked(clientRecord, route); } } } @@ -598,7 +624,7 @@ class MediaRouter2ServiceImpl { mHandler = new UserHandler(MediaRouter2ServiceImpl.this, this); } - ClientRecord findClientRecord(String packageName) { + ClientRecord findClientRecordLocked(String packageName) { for (ClientRecord clientRecord : mClientRecords) { if (TextUtils.equals(clientRecord.mPackageName, packageName)) { return clientRecord; @@ -611,12 +637,15 @@ class MediaRouter2ServiceImpl { class ClientRecord { public final UserRecord mUserRecord; public final String mPackageName; + public final List mSelectRouteSequenceNumbers; public List mControlCategories; + public MediaRoute2Info mSelectingRoute; public MediaRoute2Info mSelectedRoute; ClientRecord(UserRecord userRecord, String packageName) { mUserRecord = userRecord; mPackageName = packageName; + mSelectRouteSequenceNumbers = new ArrayList<>(); mControlCategories = Collections.emptyList(); } } @@ -752,6 +781,15 @@ class MediaRouter2ServiceImpl { sendMessage(PooledLambda.obtainMessage(UserHandler::updateProvider, this, provider)); } + // TODO: When introducing MediaRoute2ProviderService#sendControlHints(), + // Make this method to be called. + public void onRouteSelectionRequestHandled(@NonNull MediaRoute2ProviderProxy provider, + String clientPackageName, MediaRoute2Info route, Bundle controlHints, int seq) { + sendMessage(PooledLambda.obtainMessage( + UserHandler::updateSelectedRoute, this, provider, clientPackageName, route, + controlHints, seq)); + } + private void updateProvider(MediaRoute2ProviderProxy provider) { int providerIndex = getProviderInfoIndex(provider.getUniqueId()); MediaRoute2ProviderInfo providerInfo = provider.getProviderInfo(); @@ -834,24 +872,104 @@ class MediaRouter2ServiceImpl { return -1; } - private void selectRoute(ClientRecord clientRecord, MediaRoute2Info route) { + private void updateSelectedRoute(MediaRoute2ProviderProxy provider, + String clientPackageName, MediaRoute2Info selectedRoute, Bundle controlHints, + int seq) { + if (selectedRoute == null + || !TextUtils.equals(clientPackageName, selectedRoute.getClientPackageName())) { + Log.w(TAG, "Ignoring route selection which has non-matching clientPackageName."); + return; + } + + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + + ClientRecord clientRecord; + synchronized (service.mLock) { + clientRecord = mUserRecord.findClientRecordLocked(clientPackageName); + } + if (!(clientRecord instanceof Client2Record)) { + Log.w(TAG, "Ignoring route selection for unknown client."); + unselectRoute(clientPackageName, selectedRoute); + return; + } + + if (clientRecord.mSelectingRoute == null || !TextUtils.equals( + clientRecord.mSelectingRoute.getUniqueId(), selectedRoute.getUniqueId())) { + Log.w(TAG, "Ignoring invalid updateSelectedRoute call. selectingRoute=" + + clientRecord.mSelectingRoute + " route=" + selectedRoute); + unselectRoute(clientPackageName, selectedRoute); + return; + } + clientRecord.mSelectingRoute = null; + clientRecord.mSelectedRoute = selectedRoute; + + notifyRouteSelectedToClient(((Client2Record) clientRecord).mClient, + selectedRoute, + MediaRouter2.SELECT_REASON_USER_SELECTED, + controlHints); + updateClientUsage(clientRecord); + + // Remove the fallback route selection message. + removeMessages(seq); + } + + private void handleRouteSelectionTimeout(String clientPackageName, + MediaRoute2Info selectingRoute) { + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + + ClientRecord clientRecord; + synchronized (service.mLock) { + clientRecord = mUserRecord.findClientRecordLocked(clientPackageName); + } + if (!(clientRecord instanceof Client2Record)) { + Log.w(TAG, "Ignoring fallback route selection for unknown client."); + return; + } + + if (clientRecord.mSelectingRoute == null || !TextUtils.equals( + clientRecord.mSelectingRoute.getUniqueId(), selectingRoute.getUniqueId())) { + Log.w(TAG, "Ignoring invalid selectFallbackRoute call. " + + "Current selectingRoute=" + clientRecord.mSelectingRoute + + " , original selectingRoute=" + selectingRoute); + return; + } + + clientRecord.mSelectingRoute = null; + // TODO: When the default route is introduced, make mSelectedRoute always non-null. + MediaRoute2Info fallbackRoute = null; + clientRecord.mSelectedRoute = fallbackRoute; + + notifyRouteSelectedToClient(((Client2Record) clientRecord).mClient, + fallbackRoute, + MediaRouter2.SELECT_REASON_FALLBACK, + Bundle.EMPTY /* controlHints */); + updateClientUsage(clientRecord); + } + + private void requestSelectRoute(String clientPackageName, MediaRoute2Info route, int seq) { if (route != null) { MediaRoute2ProviderProxy provider = findProvider(route.getProviderId()); if (provider == null) { Slog.w(TAG, "Ignoring to select route of unknown provider " + route); } else { - provider.selectRoute(clientRecord.mPackageName, route.getId()); + provider.requestSelectRoute(clientPackageName, route.getId(), seq); } } } - private void unselectRoute(ClientRecord clientRecord, MediaRoute2Info route) { + private void unselectRoute(String clientPackageName, MediaRoute2Info route) { if (route != null) { MediaRoute2ProviderProxy provider = findProvider(route.getProviderId()); if (provider == null) { Slog.w(TAG, "Ignoring to unselect route of unknown provider " + route); } else { - provider.unselectRoute(clientRecord.mPackageName, route.getId()); + provider.unselectRoute(clientPackageName, route.getId()); } } } @@ -922,6 +1040,15 @@ class MediaRouter2ServiceImpl { } } + private void notifyRouteSelectedToClient(IMediaRouter2Client client, + MediaRoute2Info route, int reason, Bundle controlHints) { + try { + client.notifyRouteSelected(route, reason, controlHints); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify routes selected. Client probably died.", ex); + } + } + private void notifyRoutesAddedToClients(List clients, List routes) { for (IMediaRouter2Client client : clients) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index afd92f61b9d37..ecc1abaa2cdfe 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -459,8 +459,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void selectRoute2(IMediaRouter2Client client, MediaRoute2Info route) { - mService2.selectRoute2(client, route); + public void requestSelectRoute2(IMediaRouter2Client client, MediaRoute2Info route) { + mService2.requestSelectRoute2(client, route); } // Binder call