Merge "Media: Add MediaRouterManager to control media route of other apps"
diff --git a/Android.bp b/Android.bp
index d2d8a36..8a3f76c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -476,7 +476,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",
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 @@
}
}
}
+
}