Media: Add MediaRouterManager to control media route of other apps
This CL is a draft for supporting seamless transfer
MediaRouterManager is added to notify providers seamless transfer request.
It also adds MediaRouter.setControlCategories() to let application notify
their control categories and filter out irrelevant routes.
Test: atest mediaroutertest after installing mediarouteprovider.apk
Change-Id: I7446855271d27ffaad2e82dda133bed80b9f9630
diff --git a/Android.bp b/Android.bp
index 87388d4..5f0d723 100644
--- a/Android.bp
+++ b/Android.bp
@@ -474,7 +474,10 @@
"media/java/android/media/IMediaHTTPConnection.aidl",
"media/java/android/media/IMediaHTTPService.aidl",
"media/java/android/media/IMediaResourceMonitor.aidl",
+ "media/java/android/media/IMediaRoute2Callback.aidl",
+ "media/java/android/media/IMediaRoute2Provider.aidl",
"media/java/android/media/IMediaRouterClient.aidl",
+ "media/java/android/media/IMediaRouter2ManagerClient.aidl",
"media/java/android/media/IMediaRouterService.aidl",
"media/java/android/media/IMediaScannerListener.aidl",
"media/java/android/media/IMediaScannerService.aidl",
@@ -1801,4 +1804,4 @@
name: "framework-aidl-mappings",
srcs: [":framework-defaults"],
output: "framework-aidl-mappings.txt"
-}
\ No newline at end of file
+}
diff --git a/media/java/android/media/IMediaRoute2Callback.aidl b/media/java/android/media/IMediaRoute2Callback.aidl
new file mode 100644
index 0000000..f03c8ab
--- /dev/null
+++ b/media/java/android/media/IMediaRoute2Callback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019 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 IMediaRoute2Callback {
+ void onRouteSelected(int uid, String routeId);
+}
diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl
new file mode 100644
index 0000000..b97dcc5
--- /dev/null
+++ b/media/java/android/media/IMediaRoute2Provider.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 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.IMediaRoute2Callback;
+
+/**
+ * {@hide}
+ */
+oneway interface IMediaRoute2Provider {
+ void setCallback(IMediaRoute2Callback callback);
+ void selectRoute(int uid, String id);
+}
diff --git a/media/java/android/media/IMediaRouter2ManagerClient.aidl b/media/java/android/media/IMediaRouter2ManagerClient.aidl
new file mode 100644
index 0000000..234551b
--- /dev/null
+++ b/media/java/android/media/IMediaRouter2ManagerClient.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019 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 IMediaRouter2ManagerClient {
+ void onRouteSelected(int uid, String routeId);
+ void onControlCategoriesChanged(int uid, in List<String> categories);
+}
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 3308fc9..59f1d0d 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -17,6 +17,7 @@
package android.media;
import android.media.IMediaRouterClient;
+import android.media.IMediaRouter2ManagerClient;
import android.media.MediaRouterClientState;
/**
@@ -29,8 +30,15 @@
MediaRouterClientState getState(IMediaRouterClient client);
boolean isPlaybackActive(IMediaRouterClient client);
+ void setControlCategories(IMediaRouterClient client, in List<String> categories);
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);
+
+ void registerManagerAsUser(IMediaRouter2ManagerClient callback,
+ String packageName, int userId);
+ void unregisterManager(IMediaRouter2ManagerClient callback);
+ void setRemoteRoute(IMediaRouter2ManagerClient callback,
+ int uid, String routeId, boolean explicit);
}
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
new file mode 100644
index 0000000..04ddc30
--- /dev/null
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 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 static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * @hide
+ */
+public abstract class MediaRoute2ProviderService extends Service {
+ private static final String TAG = "MediaRouteProviderSrv";
+
+ public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService";
+
+ private final Handler mHandler;
+ private ProviderStub mStub;
+ private IMediaRoute2Callback mCallback;
+
+ public MediaRoute2ProviderService() {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ if (mStub == null) {
+ mStub = new ProviderStub();
+ }
+ return mStub;
+ }
+ return null;
+ }
+
+ /**
+ * Called when selectRoute is called on a route of the provider.
+ *
+ * @param uid The target application uid
+ * @param routeId The id of the target route
+ */
+ public abstract void onSelect(int uid, String routeId);
+
+ /**
+ * Updates provider info from selected route and appliation.
+ *
+ * TODO: When provider descriptor is defined, this should update the descriptor correctly.
+ *
+ * @param uid
+ * @param routeId
+ */
+ public void updateProvider(int uid, String routeId) {
+ if (mCallback != null) {
+ try {
+ //TODO: After publishState() is fully implemented, delete this.
+ mCallback.onRouteSelected(uid, routeId);
+ } catch (RemoteException ex) {
+ Log.d(TAG, "Failed to update provider");
+ }
+ }
+ publishState();
+ }
+
+ void setCallback(IMediaRoute2Callback callback) {
+ mCallback = callback;
+ publishState();
+ }
+
+ void publishState() {
+ //TODO: Send provider descriptor to the MediaRouterService
+ }
+
+ final class ProviderStub extends IMediaRoute2Provider.Stub {
+ ProviderStub() { }
+
+ @Override
+ public void setCallback(IMediaRoute2Callback callback) {
+ mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::setCallback,
+ MediaRoute2ProviderService.this, callback));
+ }
+
+ @Override
+ public void selectRoute(int uid, String id) {
+ mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelect,
+ MediaRoute2ProviderService.this, uid, id));
+ }
+ }
+}
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index 3444e92..5a89d8c 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -347,6 +347,17 @@
return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
}
+ void setControlCategories(List<String> categories) {
+ if (mClient != null) {
+ try {
+ mMediaRouterService.setControlCategories(mClient,
+ categories);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to set control categories.", ex);
+ }
+ }
+ }
+
private void updatePresentationDisplays(int changedDisplayId) {
final int count = mRoutes.size();
for (int i = 0; i < count; i++) {
@@ -919,6 +930,25 @@
return -1;
}
+ //TODO: Remove @hide when it is ready.
+ //TODO: Provide pre-defined categories for app developers.
+ /**
+ * Sets control categories of the client application.
+ * Control categories can be used to filter out media routes
+ * that don't correspond with the client application.
+ * The only routes that match any of the categories will be shown on other applications.
+ *
+ * @hide
+ * @param categories Categories to set
+ */
+ public void setControlCategories(@NonNull List<String> categories) {
+ if (categories == null) {
+ throw new IllegalArgumentException("Categories must not be null");
+ }
+ sStatic.setControlCategories(categories);
+ }
+
+
/**
* Select the specified route to use for output of the given media types.
* <p class="note">
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
new file mode 100644
index 0000000..ac5958e
--- /dev/null
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2019 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 static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class MediaRouter2Manager {
+ private static final String TAG = "MediaRouter2Manager";
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static MediaRouter2Manager sInstance;
+
+ final String mPackageName;
+
+ private Context mContext;
+ private Client mClient;
+ private final IMediaRouterService mMediaRouterService;
+ final Handler mHandler;
+
+ @GuardedBy("sLock")
+ final ArrayList<CallbackRecord> mCallbacks = new ArrayList<>();
+
+ /**
+ * Gets an instance of media router manager that controls media route of other apps.
+ * @param context
+ * @return
+ */
+ public static MediaRouter2Manager getInstance(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new MediaRouter2Manager(context);
+ }
+ return sInstance;
+ }
+ }
+
+ private MediaRouter2Manager(Context context) {
+ mContext = context.getApplicationContext();
+ mMediaRouterService = IMediaRouterService.Stub.asInterface(
+ ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
+ mPackageName = mContext.getPackageName();
+ mHandler = new Handler(context.getMainLooper());
+ }
+
+ /**
+ * Registers a callback to listen route info.
+ *
+ * @param executor The executor that runs the callback.
+ * @param callback The callback to add.
+ */
+ public void addCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Callback callback) {
+
+ if (executor == null) {
+ throw new IllegalArgumentException("executor must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ synchronized (sLock) {
+ final int index = findCallbackRecord(callback);
+ if (index >= 0) {
+ Log.w(TAG, "Ignore adding the same callback twice.");
+ return;
+ }
+ if (mCallbacks.size() == 0) {
+ Client client = new Client();
+ try {
+ mMediaRouterService.registerManagerAsUser(client, mPackageName,
+ UserHandle.myUserId());
+ mClient = client;
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to register media router manager.", ex);
+ }
+ }
+ mCallbacks.add(new CallbackRecord(executor, callback));
+ }
+ }
+
+ /**
+ * Removes the specified callback.
+ *
+ * @param callback The callback to remove.
+ */
+ public void removeCallback(@NonNull Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+
+ synchronized (sLock) {
+ final int index = findCallbackRecord(callback);
+ if (index < 0) {
+ Log.w(TAG, "Ignore removing unknown callback. " + callback);
+ return;
+ }
+ mCallbacks.remove(index);
+ if (mCallbacks.size() == 0 && mClient != null) {
+ try {
+ mMediaRouterService.unregisterManager(mClient);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to unregister media router manager", ex);
+ }
+ mClient = null;
+ }
+ }
+ }
+
+ private int findCallbackRecord(Callback callback) {
+ final int count = mCallbacks.size();
+ for (int i = 0; i < count; i++) {
+ if (mCallbacks.get(i).mCallback == callback) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Selects media route for the specified application uid.
+ *
+ * @param uid The uid of the application that should change it's media route.
+ * @param routeId The id of the route to select
+ */
+ public void selectRoute(int uid, String routeId) {
+ if (mClient != null) {
+ try {
+ mMediaRouterService.setRemoteRoute(mClient, uid, routeId, /* explicit= */true);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to select media route", ex);
+ }
+ }
+ }
+
+ /**
+ * Unselects media route for the specified application uid.
+ *
+ * @param uid The uid of the application that should stop routing.
+ */
+ public void unselectRoute(int uid) {
+ if (mClient != null) {
+ try {
+ mMediaRouterService.setRemoteRoute(mClient, uid, null, /* explicit= */ true);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to select media route", ex);
+ }
+ }
+ }
+
+ void notifyRouteSelected(int uid, String routeId) {
+ for (CallbackRecord record : mCallbacks) {
+ record.mExecutor.execute(() -> record.mCallback.onRouteSelected(uid, routeId));
+ }
+ }
+
+ void notifyControlCategoriesChanged(int uid, List<String> categories) {
+ for (CallbackRecord record : mCallbacks) {
+ record.mExecutor.execute(
+ () -> record.mCallback.onControlCategoriesChanged(uid, categories));
+ }
+ }
+
+ /**
+ * Interface for receiving events about media routing changes.
+ */
+ public abstract static class Callback {
+ /**
+ * Called when a route is selected for some application uid.
+ * @param uid
+ * @param routeId
+ */
+ public abstract void onRouteSelected(int uid, String routeId);
+
+ /**
+ * Called when the control categories of an application is changed.
+ * @param uid the uid of the app that changed control categories
+ * @param categories the changed categories
+ */
+ public abstract void onControlCategoriesChanged(int uid, List<String> categories);
+ }
+
+ final class CallbackRecord {
+ public final Executor mExecutor;
+ public final Callback mCallback;
+
+ CallbackRecord(Executor executor, Callback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+ }
+
+ class Client extends IMediaRouter2ManagerClient.Stub {
+ @Override
+ public void onRouteSelected(int uid, String routeId) {
+ mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyRouteSelected,
+ MediaRouter2Manager.this, uid, routeId));
+ }
+
+ @Override
+ public void onControlCategoriesChanged(int uid, List<String> categories) {
+ mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyControlCategoriesChanged,
+ MediaRouter2Manager.this, uid, categories));
+ }
+ }
+}
diff --git a/media/tests/MediaRouteProvider/Android.bp b/media/tests/MediaRouteProvider/Android.bp
new file mode 100644
index 0000000..da42824
--- /dev/null
+++ b/media/tests/MediaRouteProvider/Android.bp
@@ -0,0 +1,18 @@
+android_test {
+ name: "mediarouteprovider",
+
+ srcs: ["**/*.java"],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+
+ static_libs: [
+ "android-support-test",
+ "mockito-target-minus-junit4",
+ ],
+
+ platform_apis: true,
+ certificate: "platform",
+}
\ No newline at end of file
diff --git a/media/tests/MediaRouteProvider/AndroidManifest.xml b/media/tests/MediaRouteProvider/AndroidManifest.xml
new file mode 100644
index 0000000..489a621
--- /dev/null
+++ b/media/tests/MediaRouteProvider/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.mediarouteprovider.example">
+
+ <application android:label="@string/app_name">
+ <uses-library android:name="android.test.runner" />
+ <service android:name=".SampleMediaRoute2ProviderService"
+ android:label="@string/app_name"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.media.MediaRoute2ProviderService" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/media/tests/MediaRouteProvider/res/values/strings.xml b/media/tests/MediaRouteProvider/res/values/strings.xml
new file mode 100644
index 0000000..bb97064
--- /dev/null
+++ b/media/tests/MediaRouteProvider/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- name of the app [CHAR LIMIT=25]-->
+ <string name="app_name">SampleMediaRouteProvider</string>
+</resources>
\ No newline at end of file
diff --git a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
new file mode 100644
index 0000000..22fbd85
--- /dev/null
+++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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 com.android.mediarouteprovider.example;
+
+import android.content.Intent;
+import android.media.MediaRoute2ProviderService;
+import android.os.IBinder;
+
+public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService {
+ @Override
+ public IBinder onBind(Intent intent) {
+ return super.onBind(intent);
+ }
+
+ @Override
+ public void onSelect(int uid, String routeId) {
+ updateProvider(uid, routeId);
+ }
+}
diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp
new file mode 100644
index 0000000..611b25a
--- /dev/null
+++ b/media/tests/MediaRouter/Android.bp
@@ -0,0 +1,18 @@
+android_test {
+ name: "mediaroutertest",
+
+ srcs: ["**/*.java"],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+
+ static_libs: [
+ "android-support-test",
+ "mockito-target-minus-junit4",
+ ],
+
+ platform_apis: true,
+ certificate: "platform",
+}
\ No newline at end of file
diff --git a/media/tests/MediaRouter/AndroidManifest.xml b/media/tests/MediaRouter/AndroidManifest.xml
new file mode 100644
index 0000000..a34a264
--- /dev/null
+++ b/media/tests/MediaRouter/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.mediaroutertest">
+
+ <uses-permission android:name="android.permission.CONTROL_MEDIA_ROUTE" />
+
+ <application android:label="@string/app_name">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.mediaroutertest"
+ android:label="MediaRouter Tests"/>
+</manifest>
diff --git a/media/tests/MediaRouter/AndroidTest.xml b/media/tests/MediaRouter/AndroidTest.xml
new file mode 100644
index 0000000..1301062
--- /dev/null
+++ b/media/tests/MediaRouter/AndroidTest.xml
@@ -0,0 +1,16 @@
+<configuration description="Runs sample instrumentation test.">
+ <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="mediaroutertest.apk"/>
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
+ <option name="test-suite-tag" value="apct"/>
+ <option name="test-tag" value="MediaRouterTest"/>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.mediaroutertest"/>
+ <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/media/tests/MediaRouter/res/values/strings.xml b/media/tests/MediaRouter/res/values/strings.xml
new file mode 100644
index 0000000..0737020
--- /dev/null
+++ b/media/tests/MediaRouter/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- name of the app [CHAR LIMIT=25]-->
+ <string name="app_name">mediaRouterTest</string>
+</resources>
\ No newline at end of file
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
new file mode 100644
index 0000000..a4bde65
--- /dev/null
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019 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 com.android.mediaroutertest;
+
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.any;
+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.MediaRouter;
+import android.media.MediaRouter2Manager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaRouterManagerTest {
+ private static final String TAG = "MediaRouterManagerTest";
+
+ private static final int TARGET_UID = 109992;
+ private static final String ROUTE_1 = "MediaRoute1";
+
+ private static final int AWAIT_MS = 1000;
+ private static final int TIMEOUT_MS = 1000;
+
+ private Context mContext;
+ private MediaRouter2Manager mManager;
+ private MediaRouter mRouter;
+ private Executor mExecutor;
+
+ private static final List<String> TEST_CONTROL_CATEGORIES = new ArrayList();
+ private static final String CONTROL_CATEGORY_1 = "android.media.mediarouter.MEDIA1";
+ private static final String CONTROL_CATEGORY_2 = "android.media.mediarouter.MEDIA2";
+ static {
+ TEST_CONTROL_CATEGORIES.add(CONTROL_CATEGORY_1);
+ TEST_CONTROL_CATEGORIES.add(CONTROL_CATEGORY_2);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mManager = MediaRouter2Manager.getInstance(mContext);
+ mRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ mExecutor = new ThreadPoolExecutor(
+ 1, 20, 3, TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>());
+ }
+
+ @Test
+ public void transferTest() throws Exception {
+ MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class);
+
+ mManager.addCallback(mExecutor, mockCallback);
+
+ verify(mockCallback, after(AWAIT_MS).never())
+ .onRouteSelected(eq(TARGET_UID), any(String.class));
+
+ mManager.selectRoute(TARGET_UID, ROUTE_1);
+ verify(mockCallback, timeout(TIMEOUT_MS)).onRouteSelected(TARGET_UID, ROUTE_1);
+
+ mManager.removeCallback(mockCallback);
+ }
+
+ @Test
+ public void controlCategoryTest() throws Exception {
+ final int uid = android.os.Process.myUid();
+
+ MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class);
+ mManager.addCallback(mExecutor, mockCallback);
+
+ verify(mockCallback, after(AWAIT_MS).never()).onControlCategoriesChanged(eq(uid),
+ any(List.class));
+
+ mRouter.setControlCategories(TEST_CONTROL_CATEGORIES);
+ verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
+ .onControlCategoriesChanged(uid, TEST_CONTROL_CATEGORIES);
+
+ 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
new file mode 100644
index 0000000..d284c60
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2019 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 com.android.server.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.IMediaRoute2Callback;
+import android.media.IMediaRoute2Provider;
+import android.media.MediaRoute2ProviderService;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+
+/**
+ * Maintains a connection to a particular media route provider service.
+ */
+final class MediaRoute2ProviderProxy implements ServiceConnection {
+ private static final String TAG = "MediaRoute2ProviderProxy";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Context mContext;
+ private final ComponentName mComponentName;
+ private final int mUserId;
+ private final Handler mHandler;
+
+ private Callback mCallback;
+
+ // Selected Route info
+ public int mSelectedUid;
+ public String mSelectedRouteId;
+
+ // Connection state
+ private boolean mRunning;
+ private boolean mBound;
+ private Connection mActiveConnection;
+ private boolean mConnectionReady;
+
+ MediaRoute2ProviderProxy(Context context, ComponentName componentName, int userId) {
+ mContext = context;
+ mComponentName = componentName;
+ mUserId = userId;
+ mHandler = new Handler();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "Proxy");
+ pw.println(prefix + " mUserId=" + mUserId);
+ pw.println(prefix + " mRunning=" + mRunning);
+ pw.println(prefix + " mBound=" + mBound);
+ pw.println(prefix + " mActiveConnection=" + mActiveConnection);
+ pw.println(prefix + " mConnectionReady=" + mConnectionReady);
+ }
+
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ public void setSelectedRoute(int uid, String routeId) {
+ if (mConnectionReady) {
+ mActiveConnection.selectRoute(uid, routeId);
+ updateBinding();
+ }
+ }
+
+ public boolean hasComponentName(String packageName, String className) {
+ return mComponentName.getPackageName().equals(packageName)
+ && 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();
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Stopping");
+ }
+
+ mRunning = false;
+ updateBinding();
+ }
+ }
+
+ public void rebindIfDisconnected() {
+ if (mActiveConnection == null && shouldBind()) {
+ unbind();
+ bind();
+ }
+ }
+
+ private void updateBinding() {
+ if (shouldBind()) {
+ bind();
+ } else {
+ unbind();
+ }
+ }
+
+ private boolean shouldBind() {
+ //TODO: binding could be delayed until it's necessary.
+ if (mRunning) {
+ return true;
+ }
+ return false;
+ }
+
+ private void bind() {
+ if (!mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Binding");
+ }
+
+ Intent service = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
+ service.setComponent(mComponentName);
+ try {
+ mBound = mContext.bindServiceAsUser(service, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ new UserHandle(mUserId));
+ if (!mBound && DEBUG) {
+ Slog.d(TAG, this + ": Bind failed");
+ }
+ } catch (SecurityException ex) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Bind failed", ex);
+ }
+ }
+ }
+ }
+
+ private void unbind() {
+ if (mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Unbinding");
+ }
+
+ mBound = false;
+ disconnect();
+ mContext.unbindService(this);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Connected");
+ }
+
+ if (mBound) {
+ disconnect();
+
+ IMediaRoute2Provider provider = IMediaRoute2Provider.Stub.asInterface(service);
+ if (provider != null) {
+ Connection connection = new Connection(provider);
+ if (connection.register()) {
+ mActiveConnection = connection;
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Registration failed");
+ }
+ }
+ } else {
+ Slog.e(TAG, this + ": Service returned invalid remote display provider binder");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Service disconnected");
+ }
+ disconnect();
+ }
+
+ private void onConnectionReady(Connection connection) {
+ if (mActiveConnection == connection) {
+ mConnectionReady = true;
+ }
+ }
+
+ private void onConnectionDied(Connection connection) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Service connection died");
+ }
+ disconnect();
+ }
+ }
+
+ private void onRouteSelected(Connection connection, int uid, String routeId) {
+ mSelectedUid = uid;
+ mSelectedRouteId = routeId;
+
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": State changed ");
+ }
+ mHandler.post(mStateChanged);
+ }
+ }
+
+ private void disconnect() {
+ if (mActiveConnection != null) {
+ mConnectionReady = false;
+ mActiveConnection.dispose();
+ mActiveConnection = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Service connection " + mComponentName.flattenToShortString();
+ }
+
+ private final Runnable mStateChanged = new Runnable() {
+ @Override
+ public void run() {
+ if (mCallback != null) {
+ mCallback.onProviderStateChanged(MediaRoute2ProviderProxy.this);
+ }
+ }
+ };
+
+ public interface Callback {
+ void onProviderStateChanged(MediaRoute2ProviderProxy provider);
+ }
+
+ private final class Connection implements DeathRecipient {
+ private final IMediaRoute2Provider mProvider;
+ private final ProviderCallback mCallback;
+
+ Connection(IMediaRoute2Provider provider) {
+ mProvider = provider;
+ mCallback = new ProviderCallback(this);
+ }
+
+ public boolean register() {
+ try {
+ mProvider.asBinder().linkToDeath(this, 0);
+ mProvider.setCallback(mCallback);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionReady(Connection.this);
+ }
+ });
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ mProvider.asBinder().unlinkToDeath(this, 0);
+ mCallback.dispose();
+ }
+
+ public void selectRoute(int uid, String id) {
+ try {
+ mProvider.selectRoute(uid, id);
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionDied(Connection.this);
+ }
+ });
+ }
+
+ void postRouteSelected(int uid, String routeId) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onRouteSelected(Connection.this, uid, routeId);
+ }
+ });
+ }
+ }
+
+ private static final class ProviderCallback extends IMediaRoute2Callback.Stub {
+ private final WeakReference<Connection> mConnectionRef;
+
+ ProviderCallback(Connection connection) {
+ mConnectionRef = new WeakReference<Connection>(connection);
+ }
+
+ public void dispose() {
+ mConnectionRef.clear();
+ }
+
+ @Override
+ public void onRouteSelected(int uid, String routeId) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.postRouteSelected(uid, routeId);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
new file mode 100644
index 0000000..08d8c58
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2019 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 com.android.server.media;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.media.MediaRoute2ProviderService;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ */
+final class MediaRoute2ProviderWatcher {
+ private static final String TAG = "MediaRouteProvider"; // max. 23 chars
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Context mContext;
+ private final Callback mCallback;
+ private final Handler mHandler;
+ private final int mUserId;
+ private final PackageManager mPackageManager;
+
+ private final ArrayList<MediaRoute2ProviderProxy> mProviders = new ArrayList<>();
+ private boolean mRunning;
+
+ MediaRoute2ProviderWatcher(Context context,
+ Callback callback, Handler handler, int userId) {
+ mContext = context;
+ mCallback = callback;
+ mHandler = handler;
+ mUserId = userId;
+ mPackageManager = context.getPackageManager();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "Watcher");
+ pw.println(prefix + " mUserId=" + mUserId);
+ pw.println(prefix + " mRunning=" + mRunning);
+ pw.println(prefix + " mProviders.size()=" + mProviders.size());
+ }
+
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addDataScheme("package");
+ mContext.registerReceiverAsUser(mScanPackagesReceiver,
+ new UserHandle(mUserId), filter, null, mHandler);
+
+ // Scan packages.
+ // Also has the side-effect of restarting providers if needed.
+ mHandler.post(mScanPackagesRunnable);
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+
+ mContext.unregisterReceiver(mScanPackagesReceiver);
+ mHandler.removeCallbacks(mScanPackagesRunnable);
+
+ // Stop all providers.
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ mProviders.get(i).stop();
+ }
+ }
+ }
+
+ private void scanPackages() {
+ if (!mRunning) {
+ return;
+ }
+
+ // Add providers for all new services.
+ // Reorder the list so that providers left at the end will be the ones to remove.
+ int targetIndex = 0;
+ Intent intent = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServicesAsUser(
+ intent, 0, mUserId)) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo != null) {
+ int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
+ if (sourceIndex < 0) {
+ MediaRoute2ProviderProxy provider =
+ new MediaRoute2ProviderProxy(mContext,
+ new ComponentName(serviceInfo.packageName, serviceInfo.name),
+ mUserId);
+ provider.start();
+ mProviders.add(targetIndex++, provider);
+ mCallback.addProvider(provider);
+ } else if (sourceIndex >= targetIndex) {
+ MediaRoute2ProviderProxy provider = mProviders.get(sourceIndex);
+ provider.start(); // restart the provider if needed
+ provider.rebindIfDisconnected();
+ Collections.swap(mProviders, sourceIndex, targetIndex++);
+ }
+ }
+ }
+
+ // Remove providers for missing services.
+ if (targetIndex < mProviders.size()) {
+ for (int i = mProviders.size() - 1; i >= targetIndex; i--) {
+ MediaRoute2ProviderProxy provider = mProviders.get(i);
+ mCallback.removeProvider(provider);
+ mProviders.remove(provider);
+ provider.stop();
+ }
+ }
+ }
+
+ private int findProvider(String packageName, String className) {
+ int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ MediaRoute2ProviderProxy provider = mProviders.get(i);
+ if (provider.hasComponentName(packageName, className)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received package manager broadcast: " + intent);
+ }
+ scanPackages();
+ }
+ };
+
+ private final Runnable mScanPackagesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ scanPackages();
+ }
+ };
+
+ public interface Callback {
+ void addProvider(MediaRoute2ProviderProxy provider);
+ void removeProvider(MediaRoute2ProviderProxy provider);
+ }
+}
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 3eb7321..f822e82 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -16,14 +16,10 @@
package com.android.server.media;
-import com.android.internal.util.DumpUtils;
-import com.android.server.Watchdog;
-
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -34,6 +30,7 @@
import android.media.AudioSystem;
import android.media.IAudioRoutesObserver;
import android.media.IAudioService;
+import android.media.IMediaRouter2ManagerClient;
import android.media.IMediaRouterClient;
import android.media.IMediaRouterService;
import android.media.MediaRouter;
@@ -53,10 +50,14 @@
import android.util.ArrayMap;
import android.util.IntArray;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.server.Watchdog;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -97,6 +98,7 @@
private final Object mLock = new Object();
private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
private final ArrayMap<IBinder, ClientRecord> mAllClientRecords = new ArrayMap<>();
+ private final ArrayMap<IBinder, ManagerRecord> mAllManagerRecords = new ArrayMap<>();
private int mCurrentUserId = -1;
private final IAudioService mAudioService;
private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
@@ -306,6 +308,22 @@
// Binder call
@Override
+ public void setControlCategories(IMediaRouterClient client, List<String> categories) {
+ if (client == null) {
+ throw new IllegalArgumentException("client must not be null");
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setControlCategoriesLocked(client, categories);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // Binder call
+ @Override
public void setDiscoveryRequest(IMediaRouterClient client,
int routeTypes, boolean activeScan) {
if (client == null) {
@@ -404,6 +422,65 @@
}
}
+ // Binder call
+ @Override
+ public void registerManagerAsUser(IMediaRouter2ManagerClient client,
+ String packageName, int userId) {
+ if (client == null) {
+ throw new IllegalArgumentException("client must not be null");
+ }
+ //TODO: should check permission
+ final boolean trusted = true;
+
+ final int uid = Binder.getCallingUid();
+ if (!validatePackageName(uid, packageName)) {
+ throw new SecurityException("packageName must match the calling uid");
+ }
+
+ final int pid = Binder.getCallingPid();
+ final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+ false /*allowAll*/, true /*requireFull*/, "registerManagerAsUser", packageName);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ registerManagerLocked(client, uid, pid, packageName, resolvedUserId, trusted);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // Binder call
+ @Override
+ public void unregisterManager(IMediaRouter2ManagerClient client) {
+ if (client == null) {
+ throw new IllegalArgumentException("client must not be null");
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ unregisterManagerLocked(client, false);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // Binder call
+ @Override
+ public void setRemoteRoute(IMediaRouter2ManagerClient client,
+ int uid, String routeId, boolean explicit) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setRemoteRouteLocked(client, uid, routeId, explicit);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
void restoreBluetoothA2dp() {
try {
boolean a2dpOn;
@@ -475,6 +552,12 @@
}
}
+ void clientDied(ManagerRecord managerRecord) {
+ synchronized (mLock) {
+ unregisterManagerLocked(managerRecord.mClient, true);
+ }
+ }
+
private void registerClientLocked(IMediaRouterClient client,
int uid, int pid, String packageName, int userId, boolean trusted) {
final IBinder binder = client.asBinder();
@@ -522,6 +605,17 @@
return null;
}
+ private void setControlCategoriesLocked(IMediaRouterClient client, List<String> categories) {
+ final IBinder binder = client.asBinder();
+ ClientRecord clientRecord = mAllClientRecords.get(binder);
+
+ if (clientRecord != null) {
+ clientRecord.mControlCategories = categories;
+ clientRecord.mUserRecord.mHandler.obtainMessage(
+ UserHandler.MSG_UPDATE_CLIENT_USAGE, clientRecord).sendToTarget();
+ }
+ }
+
private void setDiscoveryRequestLocked(IMediaRouterClient client,
int routeTypes, boolean activeScan) {
final IBinder binder = client.asBinder();
@@ -575,6 +669,63 @@
}
}
+ private void registerManagerLocked(IMediaRouter2ManagerClient client,
+ int uid, int pid, String packageName, int userId, boolean trusted) {
+ final IBinder binder = client.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+ if (managerRecord == null) {
+ boolean newUser = false;
+ UserRecord userRecord = mUserRecords.get(userId);
+ if (userRecord == null) {
+ userRecord = new UserRecord(userId);
+ newUser = true;
+ }
+ managerRecord = new ManagerRecord(userRecord, client, uid, pid, packageName, trusted);
+ try {
+ binder.linkToDeath(managerRecord, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException("Media router client died prematurely.", ex);
+ }
+
+ if (newUser) {
+ mUserRecords.put(userId, userRecord);
+ initializeUserLocked(userRecord);
+ }
+
+ userRecord.mManagerRecords.add(managerRecord);
+ mAllManagerRecords.put(binder, managerRecord);
+
+ // send client usage to manager
+ final int clientCount = userRecord.mClientRecords.size();
+ for (int i = 0; i < clientCount; i++) {
+ userRecord.mHandler.obtainMessage(UserHandler.MSG_UPDATE_CLIENT_USAGE,
+ userRecord.mClientRecords.get(i)).sendToTarget();
+ }
+ }
+ }
+
+ private void unregisterManagerLocked(IMediaRouter2ManagerClient client, boolean died) {
+ ManagerRecord clientRecord = mAllManagerRecords.remove(client.asBinder());
+ if (clientRecord != null) {
+ UserRecord userRecord = clientRecord.mUserRecord;
+ userRecord.mManagerRecords.remove(clientRecord);
+ clientRecord.dispose();
+ disposeUserIfNeededLocked(userRecord); // since client removed from user
+ }
+ }
+
+ private void setRemoteRouteLocked(IMediaRouter2ManagerClient client,
+ int uid, String routeId, boolean explicit) {
+ ManagerRecord managerRecord = mAllManagerRecords.get(client.asBinder());
+ if (managerRecord != null) {
+ if (explicit && managerRecord.mTrusted) {
+ Pair<Integer, String> obj = new Pair<>(uid, routeId);
+ managerRecord.mUserRecord.mHandler.obtainMessage(
+ UserHandler.MSG_SELECT_REMOTE_ROUTE, obj).sendToTarget();
+ }
+ }
+ }
+
private void requestSetVolumeLocked(IMediaRouterClient client,
String routeId, int volume) {
final IBinder binder = client.asBinder();
@@ -667,6 +818,46 @@
}
}
+ final class ManagerRecord implements DeathRecipient {
+ public final UserRecord mUserRecord;
+ public final IMediaRouter2ManagerClient mClient;
+ public final int mUid;
+ public final int mPid;
+ public final String mPackageName;
+ public final boolean mTrusted;
+
+ ManagerRecord(UserRecord userRecord, IMediaRouter2ManagerClient client,
+ int uid, int pid, String packageName, boolean trusted) {
+ mUserRecord = userRecord;
+ mClient = client;
+ mUid = uid;
+ mPid = pid;
+ mPackageName = packageName;
+ mTrusted = trusted;
+ }
+
+ public void dispose() {
+ mClient.asBinder().unlinkToDeath(this, 0);
+ }
+
+ @Override
+ public void binderDied() {
+ clientDied(this);
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + this);
+
+ final String indent = prefix + " ";
+ pw.println(indent + "mTrusted=" + mTrusted);
+ }
+
+ @Override
+ public String toString() {
+ return "Client " + mPackageName + " (pid " + mPid + ")";
+ }
+ }
+
/**
* Information about a particular client of the media router.
* The contents of this object is guarded by mLock.
@@ -678,6 +869,7 @@
public final int mPid;
public final String mPackageName;
public final boolean mTrusted;
+ public List<String> mControlCategories;
public int mRouteTypes;
public boolean mActiveScan;
@@ -728,7 +920,8 @@
*/
final class UserRecord {
public final int mUserId;
- public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
+ public final ArrayList<ClientRecord> mClientRecords = new ArrayList<>();
+ public final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>();
public final UserHandler mHandler;
public MediaRouterClientState mRouterState;
@@ -783,7 +976,9 @@
*/
static final class UserHandler extends Handler
implements RemoteDisplayProviderWatcher.Callback,
- RemoteDisplayProviderProxy.Callback {
+ RemoteDisplayProviderProxy.Callback,
+ MediaRoute2ProviderWatcher.Callback,
+ MediaRoute2ProviderProxy.Callback {
public static final int MSG_START = 1;
public static final int MSG_STOP = 2;
public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3;
@@ -794,6 +989,9 @@
private static final int MSG_UPDATE_CLIENT_STATE = 8;
private static final int MSG_CONNECTION_TIMED_OUT = 9;
+ private static final int MSG_SELECT_REMOTE_ROUTE = 10;
+ private static final int MSG_UPDATE_CLIENT_USAGE = 11;
+
private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
private static final int TIMEOUT_REASON_CONNECTION_LOST = 2;
private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 3;
@@ -809,11 +1007,17 @@
private final MediaRouterService mService;
private final UserRecord mUserRecord;
private final RemoteDisplayProviderWatcher mWatcher;
+ private final MediaRoute2ProviderWatcher mMediaWatcher;
+
private final ArrayList<ProviderRecord> mProviderRecords =
new ArrayList<ProviderRecord>();
private final ArrayList<IMediaRouterClient> mTempClients =
new ArrayList<IMediaRouterClient>();
+ private final ArrayList<MediaRoute2ProviderProxy> mMediaProviders =
+ new ArrayList<>();
+ private final ArrayList<IMediaRouter2ManagerClient> mTempManagers = new ArrayList<>();
+
private boolean mRunning;
private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
private RouteRecord mSelectedRouteRecord;
@@ -828,6 +1032,8 @@
mUserRecord = userRecord;
mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this,
this, mUserRecord.mUserId);
+ mMediaWatcher = new MediaRoute2ProviderWatcher(service.mContext, this,
+ this, mUserRecord.mUserId);
}
@Override
@@ -869,6 +1075,15 @@
connectionTimedOut();
break;
}
+ case MSG_SELECT_REMOTE_ROUTE: {
+ Pair<Integer, String> obj = (Pair<Integer, String>) msg.obj;
+ selectRemoteRoute(obj.first, obj.second);
+ break;
+ }
+ case MSG_UPDATE_CLIENT_USAGE: {
+ updateClientUsage((ClientRecord) msg.obj);
+ break;
+ }
}
}
@@ -900,6 +1115,7 @@
if (!mRunning) {
mRunning = true;
mWatcher.start(); // also starts all providers
+ mMediaWatcher.start();
}
}
@@ -908,6 +1124,7 @@
mRunning = false;
unselectSelectedRoute();
mWatcher.stop(); // also stops all providers
+ mMediaWatcher.stop();
}
}
@@ -1039,6 +1256,26 @@
}
}
+ @Override
+ public void addProvider(MediaRoute2ProviderProxy provider) {
+ provider.setCallback(this);
+ mMediaProviders.add(provider);
+ }
+
+ @Override
+ public void removeProvider(MediaRoute2ProviderProxy provider) {
+ mMediaProviders.remove(provider);
+ }
+
+ @Override
+ public void onProviderStateChanged(MediaRoute2ProviderProxy provider) {
+ updateProvider(provider);
+ }
+
+ private void updateProvider(MediaRoute2ProviderProxy provider) {
+ scheduleUpdateClientState();
+ }
+
/**
* This function is called whenever the state of the selected route may have changed.
* It checks the state and updates timeouts or unselects the route as appropriate.
@@ -1149,6 +1386,17 @@
unselectSelectedRoute();
}
+ private void selectRemoteRoute(int uid, String routeId) {
+ if (routeId != null) {
+ final int providerCount = mMediaProviders.size();
+
+ //TODO: should find proper provider (currently assumes a single provider)
+ for (int i = 0; i < providerCount; ++i) {
+ mMediaProviders.get(i).setSelectedRoute(uid, routeId);
+ }
+ }
+ }
+
private void scheduleUpdateClientState() {
if (!mClientStateUpdateScheduled) {
mClientStateUpdateScheduled = true;
@@ -1166,6 +1414,15 @@
mProviderRecords.get(i).appendClientState(routerState);
}
+ //TODO: send provider info
+ int selectedUid = 0;
+ String selectedRouteId = null;
+ final int mediaCount = mMediaProviders.size();
+ for (int i = 0; i < mediaCount; i++) {
+ selectedUid = mMediaProviders.get(i).mSelectedUid;
+ selectedRouteId = mMediaProviders.get(i).mSelectedRouteId;
+ }
+
try {
synchronized (mService.mLock) {
// Update the UserRecord.
@@ -1176,6 +1433,11 @@
for (int i = 0; i < count; i++) {
mTempClients.add(mUserRecord.mClientRecords.get(i).mClient);
}
+
+ final int count2 = mUserRecord.mManagerRecords.size();
+ for (int i = 0; i < count2; i++) {
+ mTempManagers.add(mUserRecord.mManagerRecords.get(i).mClient);
+ }
}
// Notify all clients (outside of the lock).
@@ -1187,9 +1449,39 @@
Slog.w(TAG, "Failed to call onStateChanged. Client probably died.");
}
}
+ //TODO: Call proper callbacks when provider descriptor is implemented.
+ final int count2 = mTempManagers.size();
+ for (int i = 0; i < count2; i++) {
+ try {
+ mTempManagers.get(i).onRouteSelected(selectedUid, selectedRouteId);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to call onStateChanged. Manager probably died.", ex);
+ }
+ }
} finally {
// Clear the list in preparation for the next time.
mTempClients.clear();
+ mTempManagers.clear();
+ }
+ }
+
+ private void updateClientUsage(ClientRecord clientRecord) {
+ List<IMediaRouter2ManagerClient> managers = new ArrayList<>();
+ synchronized (mService.mLock) {
+ final int count = mUserRecord.mManagerRecords.size();
+ for (int i = 0; i < count; i++) {
+ managers.add(mUserRecord.mManagerRecords.get(i).mClient);
+ }
+ }
+ final int count = managers.size();
+ for (int i = 0; i < count; i++) {
+ try {
+ managers.get(i).onControlCategoriesChanged(clientRecord.mUid,
+ clientRecord.mControlCategories);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to call onControlCategoriesChanged. "
+ + "Manager probably died.", ex);
+ }
}
}
@@ -1576,4 +1868,5 @@
}
}
}
+
}