Merge "Media: add onRoute*** callback"

This commit is contained in:
Kyunglyul Hyun
2019-05-13 07:19:24 +00:00
committed by Android (Google) Code Review
8 changed files with 259 additions and 62 deletions

View File

@@ -24,5 +24,5 @@ import android.media.MediaRoute2ProviderInfo;
oneway interface IMediaRouter2Manager {
void notifyRouteSelected(int uid, String routeId);
void notifyControlCategoriesChanged(int uid, in List<String> categories);
void notifyProviderInfoUpdated(in MediaRoute2ProviderInfo info);
void notifyProviderInfosUpdated(in List<MediaRoute2ProviderInfo> providers);
}

View File

@@ -28,6 +28,7 @@ import android.text.TextUtils;
* @hide
*/
public final class MediaRoute2Info implements Parcelable {
@NonNull
public static final Creator<MediaRoute2Info> CREATOR = new Creator<MediaRoute2Info>() {
@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;

View File

@@ -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<MediaRoute2ProviderInfo> CREATOR =
new Parcelable.Creator<MediaRoute2ProviderInfo>() {
@NonNull
public static final Creator<MediaRoute2ProviderInfo> CREATOR =
new Creator<MediaRoute2ProviderInfo>() {
@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<MediaRoute2Info> mRoutes;
private final ArrayMap<String, MediaRoute2Info> mRoutes;
MediaRoute2ProviderInfo(@Nullable List<MediaRoute2Info> 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<String, MediaRoute2Info> 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<MediaRoute2Info> getRoutes() {
return Collections.unmodifiableList(mRoutes);
public Collection<MediaRoute2Info> 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<MediaRoute2Info> mRoutes;
final ArrayMap<String, MediaRoute2Info> 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.
* <p>
* 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.
* </p>
*/
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<MediaRoute2Info> 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);
}
}
}

View File

@@ -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<CallbackRecord> mCallbacks = new ArrayList<>();
@NonNull
private List<MediaRoute2ProviderInfo> 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<MediaRoute2Info> routes = provider.getRoutes();
final int index = findProviderIndex(provider);
if (index >= 0) {
final MediaRoute2ProviderInfo prevProvider = getProvider(index);
final Set<String> 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<MediaRoute2Info> 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<MediaRoute2ProviderInfo> 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<MediaRoute2ProviderInfo> info) {
mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyProviderInfosUpdated,
MediaRouter2Manager.this, info));
}
}

View File

@@ -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<Runnable>());
}
//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);
}

View File

@@ -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 ");
}

View File

@@ -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<MediaRoute2ProviderInfo> 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);

View File

@@ -501,6 +501,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub
}
}
}
mService2.switchUser();
}
void clientDied(ClientRecord clientRecord) {