diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 950246a031748..86bab878ff005 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -24,5 +24,5 @@ import android.media.MediaRoute2ProviderInfo; oneway interface IMediaRouter2Manager { void notifyRouteSelected(int uid, String routeId); void notifyControlCategoriesChanged(int uid, in List categories); - void notifyProviderInfoUpdated(in MediaRoute2ProviderInfo info); + void notifyProviderInfosUpdated(in List providers); } diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index f294b49ae00a6..03b49d9d84725 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -28,6 +28,7 @@ import android.text.TextUtils; * @hide */ public final class MediaRoute2Info implements Parcelable { + @NonNull public static final Creator CREATOR = new Creator() { @Override public MediaRoute2Info createFromParcel(Parcel in) { @@ -63,6 +64,18 @@ public final class MediaRoute2Info implements Parcelable { mExtras = in.readBundle(); } + /** + * Returns true if the route info has all of the required field + * @hide + */ + //TODO: Reconsider the validity of a route info when fields are added. + public boolean isValid() { + if (TextUtils.isEmpty(getId()) || TextUtils.isEmpty(getName())) { + return false; + } + return true; + } + @NonNull public String getId() { return mId; diff --git a/media/java/android/media/MediaRoute2ProviderInfo.java b/media/java/android/media/MediaRoute2ProviderInfo.java index 584eb832e02a6..3ed5abc17f825 100644 --- a/media/java/android/media/MediaRoute2ProviderInfo.java +++ b/media/java/android/media/MediaRoute2ProviderInfo.java @@ -20,20 +20,20 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.Objects; /** * Describes the state of a media router provider and the routes that it publishes. * @hide */ public final class MediaRoute2ProviderInfo implements Parcelable { - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + @NonNull + public static final Creator CREATOR = + new Creator() { @Override public MediaRoute2ProviderInfo createFromParcel(Parcel in) { return new MediaRoute2ProviderInfo(in); @@ -44,23 +44,63 @@ public final class MediaRoute2ProviderInfo implements Parcelable { } }; + @Nullable + private final String mUniqueId; @NonNull - private final List mRoutes; + private final ArrayMap mRoutes; - MediaRoute2ProviderInfo(@Nullable List routes) { - mRoutes = (routes == null) ? Collections.emptyList() : routes; + MediaRoute2ProviderInfo(@NonNull Builder builder) { + if (builder == null) { + throw new NullPointerException("Builder must not be null."); + } + mUniqueId = builder.mUniqueId; + mRoutes = builder.mRoutes; } MediaRoute2ProviderInfo(@NonNull Parcel src) { - mRoutes = src.createTypedArrayList(MediaRoute2Info.CREATOR); + mUniqueId = src.readString(); + ArrayMap routes = src.createTypedArrayMap(MediaRoute2Info.CREATOR); + mRoutes = (routes == null) ? ArrayMap.EMPTY : routes; + } + + /** + * Returns true if the information of the provider and all of it's routes have all + * of the required fields. + * @hide + */ + public boolean isValid() { + if (mUniqueId == null) { + return false; + } + final int count = mRoutes.size(); + for (int i = 0; i < count; i++) { + MediaRoute2Info route = mRoutes.valueAt(i); + if (route == null || !route.isValid()) { + return false; + } + } + return true; + } + + @Nullable + String getUniqueId() { + return mUniqueId; + } + + /** + * Gets the route for the given route id or null if no matching route exists. + */ + @Nullable + public MediaRoute2Info getRoute(String routeId) { + return mRoutes.get(routeId); } /** * Gets the unmodifiable list of all routes that this provider has published. */ @NonNull - public List getRoutes() { - return Collections.unmodifiableList(mRoutes); + public Collection getRoutes() { + return mRoutes.values(); } @Override @@ -70,14 +110,16 @@ public final class MediaRoute2ProviderInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeTypedList(mRoutes); + dest.writeString(mUniqueId); + dest.writeTypedArrayMap(mRoutes, flags); } @Override public String toString() { StringBuilder result = new StringBuilder() .append("MediaRouteProviderInfo { ") - .append("routes=").append(Arrays.toString(getRoutes().toArray())) + .append("uniqueId=").append(mUniqueId) + .append(", routes=").append(Arrays.toString(getRoutes().toArray())) .append(" }"); return result.toString(); } @@ -87,31 +129,43 @@ public final class MediaRoute2ProviderInfo implements Parcelable { */ public static final class Builder { @NonNull - private final List mRoutes; + final ArrayMap mRoutes; + String mUniqueId; public Builder() { - mRoutes = new ArrayList<>(); + mRoutes = new ArrayMap<>(); } public Builder(@NonNull MediaRoute2ProviderInfo descriptor) { if (descriptor == null) { throw new IllegalArgumentException("descriptor must not be null"); } - mRoutes = new ArrayList<>(descriptor.mRoutes); + mRoutes = new ArrayMap<>(descriptor.mRoutes); + } + + /** + * Sets the unique id of the provider info. + *

+ * The unique id is automatically set by + * {@link com.android.server.media.MediaRouterService} and used to identify providers. + * The id set by {@link MediaRoute2ProviderService} will be ignored. + *

+ */ + public Builder setUniqueId(@Nullable String uniqueId) { + mUniqueId = uniqueId; + return this; } /** * Adds a route to the provider */ public Builder addRoute(@NonNull MediaRoute2Info route) { - if (route == null) { - throw new IllegalArgumentException("route must not be null"); - } + Objects.requireNonNull(route, "route must not be null"); - if (mRoutes.contains(route)) { + if (mRoutes.containsValue(route)) { throw new IllegalArgumentException("route descriptor already added"); } - mRoutes.add(route); + mRoutes.put(route.getId(), route); return this; } @@ -119,9 +173,7 @@ public final class MediaRoute2ProviderInfo implements Parcelable { * Adds a list of routes to the provider */ public Builder addRoutes(@NonNull Collection routes) { - if (routes == null) { - throw new IllegalArgumentException("routes must not be null"); - } + Objects.requireNonNull(routes, "routes must not be null"); if (!routes.isEmpty()) { for (MediaRoute2Info route : routes) { @@ -136,7 +188,7 @@ public final class MediaRoute2ProviderInfo implements Parcelable { */ @NonNull public MediaRoute2ProviderInfo build() { - return new MediaRoute2ProviderInfo(mRoutes); + return new MediaRoute2ProviderInfo(this); } } } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 9f72e1bfa602e..0e16af36ff17a 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -25,12 +25,17 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; /** @@ -53,6 +58,9 @@ public class MediaRouter2Manager { @GuardedBy("sLock") final ArrayList mCallbacks = new ArrayList<>(); + @NonNull + private List mProviders = Collections.emptyList(); + /** * Gets an instance of media router manager that controls media route of other apps. * @param context @@ -183,11 +191,88 @@ public class MediaRouter2Manager { } } - void notifyProviderUpdated(MediaRoute2ProviderInfo info) { - //TODO: should call back properly - for (CallbackRecord record : mCallbacks) { - record.mExecutor.execute(() -> record.mCallback.onProviderInfoUpdated(info)); + int findProviderIndex(MediaRoute2ProviderInfo provider) { + final int count = mProviders.size(); + for (int i = 0; i < count; i++) { + if (TextUtils.equals(mProviders.get(i).getUniqueId(), provider.getUniqueId())) { + return i; + } } + return -1; + } + + MediaRoute2ProviderInfo getProvider(int index) { + return mProviders.get(index); + } + + void updateProvider(@NonNull MediaRoute2ProviderInfo provider) { + if (provider == null || !provider.isValid()) { + Log.w(TAG, "Ignoring invalid provider : " + provider); + return; + } + + final Collection routes = provider.getRoutes(); + + final int index = findProviderIndex(provider); + if (index >= 0) { + final MediaRoute2ProviderInfo prevProvider = getProvider(index); + final Set updatedRouteIds = new HashSet<>(); + for (MediaRoute2Info routeInfo : routes) { + final MediaRoute2Info prevRoute = prevProvider.getRoute(routeInfo.getId()); + if (prevRoute == null) { + notifyRouteAdded(routeInfo); + } else { + //TODO: Notify only it's really changed. + notifyRouteChanged(routeInfo); + updatedRouteIds.add(routeInfo.getId()); + } + } + final Collection prevRoutes = prevProvider.getRoutes(); + + for (MediaRoute2Info prevRoute : prevRoutes) { + notifyRouteRemoved(prevRoute); + } + } else { + for (MediaRoute2Info routeInfo: routes) { + notifyRouteAdded(routeInfo); + } + } + } + + void notifyRouteAdded(MediaRoute2Info routeInfo) { + for (CallbackRecord record : mCallbacks) { + record.mExecutor.execute( + () -> record.mCallback.onRouteAdded(routeInfo)); + } + } + + void notifyRouteChanged(MediaRoute2Info routeInfo) { + for (CallbackRecord record : mCallbacks) { + record.mExecutor.execute( + () -> record.mCallback.onRouteChanged(routeInfo)); + } + } + + void notifyRouteRemoved(MediaRoute2Info routeInfo) { + for (CallbackRecord record : mCallbacks) { + record.mExecutor.execute( + () -> record.mCallback.onRouteRemoved(routeInfo)); + } + } + + void notifyProviderInfosUpdated(List providers) { + if (providers == null) { + Log.w(TAG, "Providers info is null."); + return; + } + + for (MediaRoute2ProviderInfo provider : providers) { + updateProvider(provider); + } + //TODO: Call notifyProviderRemoved for removed providers. + + //TODO: Filter invalid providers. + mProviders = providers; } void notifyRouteSelected(int uid, String routeId) { @@ -207,6 +292,21 @@ public class MediaRouter2Manager { * Interface for receiving events about media routing changes. */ public abstract static class Callback { + /** + * Called when a route is added. + */ + public void onRouteAdded(MediaRoute2Info routeInfo) {} + + /** + * Called when a route is changed. + */ + public void onRouteChanged(MediaRoute2Info routeInfo) {} + + /** + * Called when a route is removed. + */ + public void onRouteRemoved(MediaRoute2Info routeInfo) {} + /** * Called when a route is selected for some application uid. * @param uid @@ -252,8 +352,8 @@ public class MediaRouter2Manager { } @Override - public void notifyProviderInfoUpdated(MediaRoute2ProviderInfo info) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyProviderUpdated, + public void notifyProviderInfosUpdated(List info) { + mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyProviderInfosUpdated, MediaRouter2Manager.this, info)); } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java index a816b9802c65d..de30e6c91da7e 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java @@ -16,16 +16,16 @@ package com.android.mediaroutertest; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.after; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import android.content.Context; -import android.media.MediaRoute2ProviderInfo; +import android.media.MediaRoute2Info; import android.media.MediaRouter; import android.media.MediaRouter2Manager; import android.support.test.InstrumentationRegistry; @@ -49,10 +49,13 @@ public class MediaRouterManagerTest { private static final String TAG = "MediaRouterManagerTest"; private static final int TARGET_UID = 109992; - private static final String ROUTE_1 = "MediaRoute1"; + + // Must be the same as SampleMediaRoute2ProviderService + public static final String ROUTE_ID1 = "route_id"; + public static final String ROUTE_NAME1 = "route_name"; private static final int AWAIT_MS = 1000; - private static final int TIMEOUT_MS = 1000; + private static final int TIMEOUT_MS = 5000; private Context mContext; private MediaRouter2Manager mManager; @@ -77,16 +80,16 @@ public class MediaRouterManagerTest { new SynchronousQueue()); } + //TODO: onRouteChanged, onRouteRemoved must be tested @Test - public void providerTest() { + public void testRouteAddedOnce() { MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class); mManager.addCallback(mExecutor, mockCallback); - //TODO: should be changed to onRouteAdded - verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce()) - .onProviderInfoUpdated(argThat( - (MediaRoute2ProviderInfo info) -> info.getRoutes().size() == 1)); + verify(mockCallback, timeout(TIMEOUT_MS)).onRouteAdded(argThat( + (MediaRoute2Info info) -> + info.getId().equals(ROUTE_ID1) && info.getName().equals(ROUTE_NAME1))); mManager.removeCallback(mockCallback); } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java index ba9ff6ad95c2a..51c16c8f7d46a 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java @@ -16,6 +16,7 @@ package com.android.server.media; +import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -34,6 +35,7 @@ import android.util.Slog; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.util.Objects; /** * Maintains a connection to a particular media route provider service. @@ -44,6 +46,7 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { private final Context mContext; private final ComponentName mComponentName; + private final String mUniqueId; private final int mUserId; private final Handler mHandler; @@ -58,9 +61,11 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { private Connection mActiveConnection; private boolean mConnectionReady; - MediaRoute2ProviderProxy(Context context, ComponentName componentName, int userId) { - mContext = context; - mComponentName = componentName; + MediaRoute2ProviderProxy(@NonNull Context context, @NonNull ComponentName componentName, + int userId) { + mContext = Objects.requireNonNull(context, "Context must not be null."); + mComponentName = Objects.requireNonNull(componentName, "Component name must not be null."); + mUniqueId = componentName.flattenToShortString(); mUserId = userId; mHandler = new Handler(); } @@ -94,16 +99,11 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { && mComponentName.getClassName().equals(className); } - public String getFlattenedComponentName() { - return mComponentName.flattenToShortString(); - } - public void start() { if (!mRunning) { if (DEBUG) { Slog.d(TAG, this + ": Starting"); } - mRunning = true; updateBinding(); } @@ -114,7 +114,6 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { if (DEBUG) { Slog.d(TAG, this + ": Stopping"); } - mRunning = false; updateBinding(); } @@ -136,7 +135,7 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { } private boolean shouldBind() { - //TODO: binding could be delayed until it's necessary. + //TODO: Binding could be delayed until it's necessary. if (mRunning) { return true; } @@ -241,7 +240,10 @@ final class MediaRoute2ProviderProxy implements ServiceConnection { if (mActiveConnection != connection) { return; } - mProviderInfo = info; + // Set a unique provider id for identifying providers. + mProviderInfo = new MediaRoute2ProviderInfo.Builder(info) + .setUniqueId(mUniqueId) + .build(); if (DEBUG) { Slog.d(TAG, this + ": 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 5d89a9d62dea2..8f6a6a97e082d 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -24,6 +24,7 @@ import android.media.MediaRoute2ProviderInfo; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.ArrayMap; @@ -118,6 +119,28 @@ class MediaRouter2ServiceImpl { } } + //TODO: Review this is handling multi-user properly. + void switchUser() { + synchronized (mLock) { + int userId = ActivityManager.getCurrentUser(); + if (mCurrentUserId != userId) { + final int oldUserId = mCurrentUserId; + mCurrentUserId = userId; // do this first + + UserRecord oldUser = mUserRecords.get(oldUserId); + if (oldUser != null) { + oldUser.mHandler.sendEmptyMessage(MediaRouterService.UserHandler.MSG_STOP); + disposeUserIfNeededLocked(oldUser); // since no longer current user + } + + UserRecord newUser = mUserRecords.get(userId); + if (newUser != null) { + newUser.mHandler.sendEmptyMessage(MediaRouterService.UserHandler.MSG_START); + } + } + } + } + void clientDied(ManagerRecord managerRecord) { synchronized (mLock) { unregisterManagerLocked(managerRecord.mClient, true); @@ -289,6 +312,7 @@ class MediaRouter2ServiceImpl { private boolean mManagerStateUpdateScheduled; UserHandler(MediaRouter2ServiceImpl service, UserRecord userRecord) { + super(Looper.getMainLooper(), null, true); mServiceRef = new WeakReference<>(service); mUserRecord = userRecord; mWatcher = new MediaRoute2ProviderWatcher(service.mContext, this, @@ -383,14 +407,17 @@ class MediaRouter2ServiceImpl { if (service == null) { return; } - - //TODO: send provider info - MediaRoute2ProviderInfo providerInfo = null; - int selectedUid = 0; - String selectedRouteId = null; + //TODO: Consider using a member variable (like mTempManagers). + final List providers = new ArrayList<>(); final int mediaCount = mMediaProviders.size(); for (int i = 0; i < mediaCount; i++) { - providerInfo = mMediaProviders.get(i).getProviderInfo(); + final MediaRoute2ProviderInfo providerInfo = + mMediaProviders.get(i).getProviderInfo(); + if (providerInfo == null || !providerInfo.isValid()) { + Log.w(TAG, "Ignoring invalid provider info : " + providerInfo); + } else { + providers.add(providerInfo); + } } try { @@ -400,13 +427,12 @@ class MediaRouter2ServiceImpl { mTempManagers.add(mUserRecord.mManagerRecords.get(i).mClient); } } - - //TODO: Call proper callbacks when provider descriptor is implemented. - if (providerInfo != null) { + //TODO: Call !proper callbacks when provider descriptor is implemented. + if (!providers.isEmpty()) { final int count = mTempManagers.size(); for (int i = 0; i < count; i++) { try { - mTempManagers.get(i).notifyProviderInfoUpdated(providerInfo); + mTempManagers.get(i).notifyProviderInfosUpdated(providers); } catch (RemoteException ex) { Slog.w(TAG, "Failed to call onStateChanged. Manager probably died.", ex); diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 69d125558cd7e..fcfa7835509de 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -501,6 +501,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } } + mService2.switchUser(); } void clientDied(ClientRecord clientRecord) {