From d51666de42ea17eba91ca59fb930b0e1a46e502e Mon Sep 17 00:00:00 2001 From: Kyunglyul Hyun Date: Thu, 11 Apr 2019 04:08:40 +0000 Subject: [PATCH] DO NOT MERGE: Revert "DO NOT MERGE: Revert "Media: Add MediaRouterManager to control media route of other apps"" This reverts commit dd3f305bb3111eb91583ced44f5f10d76e409878. Reason for revert: wrong branch Change-Id: Ic9b6d17af8e967ec73a9dac141c60e02195a8d19 --- Android.bp | 5 +- .../android/media/IMediaRoute2Callback.aidl | 24 ++ .../android/media/IMediaRoute2Provider.aidl | 27 ++ .../media/IMediaRouter2ManagerClient.aidl | 25 ++ .../android/media/IMediaRouterService.aidl | 8 + .../media/MediaRoute2ProviderService.java | 108 ++++++ media/java/android/media/MediaRouter.java | 30 ++ .../android/media/MediaRouter2Manager.java | 241 +++++++++++++ media/tests/MediaRouteProvider/Android.bp | 18 + .../MediaRouteProvider/AndroidManifest.xml | 30 ++ .../MediaRouteProvider/res/values/strings.xml | 5 + .../SampleMediaRoute2ProviderService.java | 33 ++ media/tests/MediaRouter/Android.bp | 18 + media/tests/MediaRouter/AndroidManifest.xml | 29 ++ media/tests/MediaRouter/AndroidTest.xml | 16 + .../tests/MediaRouter/res/values/strings.xml | 5 + .../MediaRouterManagerTest.java | 110 ++++++ .../media/MediaRoute2ProviderProxy.java | 341 ++++++++++++++++++ .../media/MediaRoute2ProviderWatcher.java | 176 +++++++++ .../server/media/MediaRouterService.java | 298 ++++++++++++++- 20 files changed, 1544 insertions(+), 3 deletions(-) create mode 100644 media/java/android/media/IMediaRoute2Callback.aidl create mode 100644 media/java/android/media/IMediaRoute2Provider.aidl create mode 100644 media/java/android/media/IMediaRouter2ManagerClient.aidl create mode 100644 media/java/android/media/MediaRoute2ProviderService.java create mode 100644 media/java/android/media/MediaRouter2Manager.java create mode 100644 media/tests/MediaRouteProvider/Android.bp create mode 100644 media/tests/MediaRouteProvider/AndroidManifest.xml create mode 100644 media/tests/MediaRouteProvider/res/values/strings.xml create mode 100644 media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java create mode 100644 media/tests/MediaRouter/Android.bp create mode 100644 media/tests/MediaRouter/AndroidManifest.xml create mode 100644 media/tests/MediaRouter/AndroidTest.xml create mode 100644 media/tests/MediaRouter/res/values/strings.xml create mode 100644 media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java create mode 100644 services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java create mode 100644 services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java diff --git a/Android.bp b/Android.bp index df90606e89ed7..21054ddd89c6e 100644 --- a/Android.bp +++ b/Android.bp @@ -479,7 +479,10 @@ java_defaults { "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", @@ -1832,4 +1835,4 @@ aidl_mapping { 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 0000000000000..f03c8ab444c40 --- /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 0000000000000..b97dcc521a048 --- /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 0000000000000..234551b37a10b --- /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 categories); +} diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 3308fc929b035..59f1d0dcbed8a 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 @@ interface IMediaRouterService { MediaRouterClientState getState(IMediaRouterClient client); boolean isPlaybackActive(IMediaRouterClient client); + void setControlCategories(IMediaRouterClient client, in List 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 0000000000000..04ddc3089b91e --- /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 3444e92779490..5a89d8c82f3a0 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -347,6 +347,17 @@ public class MediaRouter { return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); } + void setControlCategories(List 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 @@ public class MediaRouter { 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 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. *

diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java new file mode 100644 index 0000000000000..ac5958ea9aa16 --- /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 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 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 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 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 0000000000000..da4282495ac2f --- /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 0000000000000..489a6214ecbdb --- /dev/null +++ b/media/tests/MediaRouteProvider/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/media/tests/MediaRouteProvider/res/values/strings.xml b/media/tests/MediaRouteProvider/res/values/strings.xml new file mode 100644 index 0000000000000..bb970641ffdab --- /dev/null +++ b/media/tests/MediaRouteProvider/res/values/strings.xml @@ -0,0 +1,5 @@ + + + + SampleMediaRouteProvider + \ 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 0000000000000..22fbd85d69798 --- /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 0000000000000..611b25a2f128c --- /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 0000000000000..a34a264e12474 --- /dev/null +++ b/media/tests/MediaRouter/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/media/tests/MediaRouter/AndroidTest.xml b/media/tests/MediaRouter/AndroidTest.xml new file mode 100644 index 0000000000000..1301062db496b --- /dev/null +++ b/media/tests/MediaRouter/AndroidTest.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/media/tests/MediaRouter/res/values/strings.xml b/media/tests/MediaRouter/res/values/strings.xml new file mode 100644 index 0000000000000..07370207a3c9d --- /dev/null +++ b/media/tests/MediaRouter/res/values/strings.xml @@ -0,0 +1,5 @@ + + + + mediaRouterTest + \ 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 0000000000000..a4bde65925161 --- /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 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()); + } + + @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 0000000000000..d284c6091f731 --- /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 mConnectionRef; + + ProviderCallback(Connection connection) { + mConnectionRef = new WeakReference(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 0000000000000..08d8c58f6f872 --- /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 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 23d3ce063a3ef..a43533f35cd9f 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -30,6 +30,7 @@ import android.media.AudioRoutesInfo; 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; @@ -49,6 +50,7 @@ import android.text.TextUtils; 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; @@ -96,6 +98,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final Object mLock = new Object(); private final SparseArray mUserRecords = new SparseArray<>(); private final ArrayMap mAllClientRecords = new ArrayMap<>(); + private final ArrayMap mAllManagerRecords = new ArrayMap<>(); private int mCurrentUserId = -1; private final IAudioService mAudioService; private final AudioPlayerStateMonitor mAudioPlayerStateMonitor; @@ -302,6 +305,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + // Binder call + @Override + public void setControlCategories(IMediaRouterClient client, List 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, @@ -402,6 +421,65 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + // 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; @@ -473,6 +551,12 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + 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(); @@ -520,6 +604,17 @@ public final class MediaRouterService extends IMediaRouterService.Stub return null; } + private void setControlCategoriesLocked(IMediaRouterClient client, List 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(); @@ -573,6 +668,63 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + 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 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(); @@ -665,6 +817,46 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + 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. @@ -676,6 +868,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub public final int mPid; public final String mPackageName; public final boolean mTrusted; + public List mControlCategories; public int mRouteTypes; public boolean mActiveScan; @@ -726,7 +919,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub */ final class UserRecord { public final int mUserId; - public final ArrayList mClientRecords = new ArrayList(); + public final ArrayList mClientRecords = new ArrayList<>(); + public final ArrayList mManagerRecords = new ArrayList<>(); public final UserHandler mHandler; public MediaRouterClientState mRouterState; @@ -781,7 +975,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub */ 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; @@ -792,6 +988,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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; @@ -807,11 +1006,17 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final MediaRouterService mService; private final UserRecord mUserRecord; private final RemoteDisplayProviderWatcher mWatcher; + private final MediaRoute2ProviderWatcher mMediaWatcher; + private final ArrayList mProviderRecords = new ArrayList(); private final ArrayList mTempClients = new ArrayList(); + private final ArrayList mMediaProviders = + new ArrayList<>(); + private final ArrayList mTempManagers = new ArrayList<>(); + private boolean mRunning; private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE; private RouteRecord mSelectedRouteRecord; @@ -826,6 +1031,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub mUserRecord = userRecord; mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this, this, mUserRecord.mUserId); + mMediaWatcher = new MediaRoute2ProviderWatcher(service.mContext, this, + this, mUserRecord.mUserId); } @Override @@ -867,6 +1074,15 @@ public final class MediaRouterService extends IMediaRouterService.Stub connectionTimedOut(); break; } + case MSG_SELECT_REMOTE_ROUTE: { + Pair obj = (Pair) msg.obj; + selectRemoteRoute(obj.first, obj.second); + break; + } + case MSG_UPDATE_CLIENT_USAGE: { + updateClientUsage((ClientRecord) msg.obj); + break; + } } } @@ -898,6 +1114,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub if (!mRunning) { mRunning = true; mWatcher.start(); // also starts all providers + mMediaWatcher.start(); } } @@ -906,6 +1123,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub mRunning = false; unselectSelectedRoute(); mWatcher.stop(); // also stops all providers + mMediaWatcher.stop(); } } @@ -1037,6 +1255,26 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } + @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. @@ -1147,6 +1385,17 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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; @@ -1164,6 +1413,15 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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. @@ -1174,6 +1432,11 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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). @@ -1185,9 +1448,39 @@ public final class MediaRouterService extends IMediaRouterService.Stub 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 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); + } } } @@ -1574,4 +1867,5 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } } + }