All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.media.MediaRoute2ProviderService Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Base class for media route provider services.
 * 

* Media route provider services are used to publish {@link MediaRoute2Info media routes} such as * speakers, TVs, etc. The routes are published by calling {@link #notifyRoutes(Collection)}. * Media apps which use {@link MediaRouter2} can request to play their media on the routes. *

* When {@link MediaRouter2 media router} wants to play media on a route, * {@link #onCreateSession(long, String, String, Bundle)} will be called to handle the request. * A session can be considered as a group of currently selected routes for each connection. * Create and manage the sessions by yourself, and notify the {@link RoutingSessionInfo * session infos} when there are any changes. *

* The system media router service will bind to media route provider services when a * {@link RouteDiscoveryPreference discovery preference} is registered via * a {@link MediaRouter2 media router} by an application. See * {@link #onDiscoveryPreferenceChanged(RouteDiscoveryPreference)} for the details. *

* Use {@link #notifyRequestFailed(long, int)} to notify the failure with previously received * request ID. */ public abstract class MediaRoute2ProviderService extends Service { private static final String TAG = "MR2ProviderService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** * The {@link Intent} action that must be declared as handled by the service. * Put this in your manifest to provide media routes. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService"; /** * The request ID to pass {@link #notifySessionCreated(long, RoutingSessionInfo)} * when {@link MediaRoute2ProviderService} created a session although there was no creation * request. * * @see #notifySessionCreated(long, RoutingSessionInfo) */ public static final long REQUEST_ID_NONE = 0; /** * The request has failed due to unknown reason. * * @see #notifyRequestFailed(long, int) */ public static final int REASON_UNKNOWN_ERROR = 0; /** * The request has failed since this service rejected the request. * * @see #notifyRequestFailed(long, int) */ public static final int REASON_REJECTED = 1; /** * The request has failed due to a network error. * * @see #notifyRequestFailed(long, int) */ public static final int REASON_NETWORK_ERROR = 2; /** * The request has failed since the requested route is no longer available. * * @see #notifyRequestFailed(long, int) */ public static final int REASON_ROUTE_NOT_AVAILABLE = 3; /** * The request has failed since the request is not valid. For example, selecting a route * which is not selectable. * * @see #notifyRequestFailed(long, int) */ public static final int REASON_INVALID_COMMAND = 4; /** * @hide */ @IntDef(prefix = "REASON_", value = { REASON_UNKNOWN_ERROR, REASON_REJECTED, REASON_NETWORK_ERROR, REASON_ROUTE_NOT_AVAILABLE, REASON_INVALID_COMMAND }) @Retention(RetentionPolicy.SOURCE) public @interface Reason {} private static final int MAX_REQUEST_IDS_SIZE = 500; private final Handler mHandler; private final Object mSessionLock = new Object(); private final Object mRequestIdsLock = new Object(); private final AtomicBoolean mStatePublishScheduled = new AtomicBoolean(false); private final AtomicBoolean mSessionUpdateScheduled = new AtomicBoolean(false); private MediaRoute2ProviderServiceStub mStub; private IMediaRoute2ProviderServiceCallback mRemoteCallback; private volatile MediaRoute2ProviderInfo mProviderInfo; @GuardedBy("mRequestIdsLock") private final Deque mRequestIds = new ArrayDeque<>(MAX_REQUEST_IDS_SIZE); @GuardedBy("mSessionLock") private final ArrayMap mSessionInfo = new ArrayMap<>(); public MediaRoute2ProviderService() { mHandler = new Handler(Looper.getMainLooper()); } /** * If overriding this method, call through to the super method for any unknown actions. *

* {@inheritDoc} */ @CallSuper @Override @Nullable public IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { if (mStub == null) { mStub = new MediaRoute2ProviderServiceStub(); } return mStub; } return null; } /** * Called when a volume setting is requested on a route of the provider * * @param requestId the ID of this request * @param routeId the ID of the route * @param volume the target volume * @see MediaRoute2Info.Builder#setVolume(int) */ public abstract void onSetRouteVolume(long requestId, @NonNull String routeId, int volume); /** * Called when {@link MediaRouter2.RoutingController#setVolume(int)} is called on * a routing session of the provider * * @param requestId the ID of this request * @param sessionId the ID of the routing session * @param volume the target volume * @see RoutingSessionInfo.Builder#setVolume(int) */ public abstract void onSetSessionVolume(long requestId, @NonNull String sessionId, int volume); /** * Gets information of the session with the given id. * * @param sessionId the ID of the session * @return information of the session with the given id. * null if the session is released or ID is not valid. */ @Nullable public final RoutingSessionInfo getSessionInfo(@NonNull String sessionId) { if (TextUtils.isEmpty(sessionId)) { throw new IllegalArgumentException("sessionId must not be empty"); } synchronized (mSessionLock) { return mSessionInfo.get(sessionId); } } /** * Gets the list of {@link RoutingSessionInfo session info} that the provider service maintains. */ @NonNull public final List getAllSessionInfo() { synchronized (mSessionLock) { return new ArrayList<>(mSessionInfo.values()); } } /** * Notifies clients of that the session is created and ready for use. *

* If this session is created without any creation request, use {@link #REQUEST_ID_NONE} * as the request ID. * * @param requestId the ID of the previous request to create this session provided in * {@link #onCreateSession(long, String, String, Bundle)}. Can be * {@link #REQUEST_ID_NONE} if this session is created without any request. * @param sessionInfo information of the new session. * The {@link RoutingSessionInfo#getId() id} of the session must be unique. * @see #onCreateSession(long, String, String, Bundle) * @see #getSessionInfo(String) */ public final void notifySessionCreated(long requestId, @NonNull RoutingSessionInfo sessionInfo) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); if (DEBUG) { Log.d(TAG, "notifySessionCreated: Creating a session. requestId=" + requestId + ", sessionInfo=" + sessionInfo); } if (requestId != REQUEST_ID_NONE && !removeRequestId(requestId)) { Log.w(TAG, "notifySessionCreated: The requestId doesn't exist. requestId=" + requestId); return; } String sessionId = sessionInfo.getId(); synchronized (mSessionLock) { if (mSessionInfo.containsKey(sessionId)) { Log.w(TAG, "notifySessionCreated: Ignoring duplicate session id."); return; } mSessionInfo.put(sessionInfo.getId(), sessionInfo); if (mRemoteCallback == null) { return; } try { mRemoteCallback.notifySessionCreated(requestId, sessionInfo); } catch (RemoteException ex) { Log.w(TAG, "Failed to notify session created."); } } } /** * Notifies the existing session is updated. For example, when * {@link RoutingSessionInfo#getSelectedRoutes() selected routes} are changed. */ public final void notifySessionUpdated(@NonNull RoutingSessionInfo sessionInfo) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); if (DEBUG) { Log.d(TAG, "notifySessionUpdated: Updating session id=" + sessionInfo); } String sessionId = sessionInfo.getId(); synchronized (mSessionLock) { if (mSessionInfo.containsKey(sessionId)) { mSessionInfo.put(sessionId, sessionInfo); } else { Log.w(TAG, "notifySessionUpdated: Ignoring unknown session info."); return; } } scheduleUpdateSessions(); } /** * Notifies that the session is released. * * @param sessionId the ID of the released session. * @see #onReleaseSession(long, String) */ public final void notifySessionReleased(@NonNull String sessionId) { if (TextUtils.isEmpty(sessionId)) { throw new IllegalArgumentException("sessionId must not be empty"); } if (DEBUG) { Log.d(TAG, "notifySessionReleased: Releasing session id=" + sessionId); } RoutingSessionInfo sessionInfo; synchronized (mSessionLock) { sessionInfo = mSessionInfo.remove(sessionId); if (sessionInfo == null) { Log.w(TAG, "notifySessionReleased: Ignoring unknown session info."); return; } if (mRemoteCallback == null) { return; } try { mRemoteCallback.notifySessionReleased(sessionInfo); } catch (RemoteException ex) { Log.w(TAG, "Failed to notify session released.", ex); } } } /** * Notifies to the client that the request has failed. * * @param requestId the ID of the previous request * @param reason the reason why the request has failed * * @see #REASON_UNKNOWN_ERROR * @see #REASON_REJECTED * @see #REASON_NETWORK_ERROR * @see #REASON_ROUTE_NOT_AVAILABLE * @see #REASON_INVALID_COMMAND */ public final void notifyRequestFailed(long requestId, @Reason int reason) { if (mRemoteCallback == null) { return; } if (!removeRequestId(requestId)) { Log.w(TAG, "notifyRequestFailed: The requestId doesn't exist. requestId=" + requestId); return; } try { mRemoteCallback.notifyRequestFailed(requestId, reason); } catch (RemoteException ex) { Log.w(TAG, "Failed to notify that the request has failed."); } } /** * Called when the service receives a request to create a session. *

* You should create and maintain your own session and notifies the client of * session info. Call {@link #notifySessionCreated(long, RoutingSessionInfo)} * with the given {@code requestId} to notify the information of a new session. * The created session must have the same route feature and must include the given route * specified by {@code routeId}. *

* If the session can be controlled, you can optionally pass the control hints to * {@link RoutingSessionInfo.Builder#setControlHints(Bundle)}. Control hints is a * {@link Bundle} which contains how to control the session. *

* If you can't create the session or want to reject the request, call * {@link #notifyRequestFailed(long, int)} with the given {@code requestId}. * * @param requestId the ID of this request * @param packageName the package name of the application that selected the route * @param routeId the ID of the route initially being connected * @param sessionHints an optional bundle of app-specific arguments sent by * {@link MediaRouter2}, or null if none. The contents of this bundle * may affect the result of session creation. * * @see RoutingSessionInfo.Builder#Builder(String, String) * @see RoutingSessionInfo.Builder#addSelectedRoute(String) * @see RoutingSessionInfo.Builder#setControlHints(Bundle) */ public abstract void onCreateSession(long requestId, @NonNull String packageName, @NonNull String routeId, @Nullable Bundle sessionHints); /** * Called when the session should be released. A client of the session or system can request * a session to be released. *

* After releasing the session, call {@link #notifySessionReleased(String)} * with the ID of the released session. * * Note: Calling {@link #notifySessionReleased(String)} will NOT trigger * this method to be called. * * @param requestId the ID of this request * @param sessionId the ID of the session being released. * @see #notifySessionReleased(String) * @see #getSessionInfo(String) */ public abstract void onReleaseSession(long requestId, @NonNull String sessionId); /** * Called when a client requests selecting a route for the session. * After the route is selected, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * * @param requestId the ID of this request * @param sessionId the ID of the session * @param routeId the ID of the route */ public abstract void onSelectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); /** * Called when a client requests deselecting a route from the session. * After the route is deselected, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * * @param requestId the ID of this request * @param sessionId the ID of the session * @param routeId the ID of the route */ public abstract void onDeselectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); /** * Called when a client requests transferring a session to a route. * After the transfer is finished, call {@link #notifySessionUpdated(RoutingSessionInfo)} * to update session info. * * @param requestId the ID of this request * @param sessionId the ID of the session * @param routeId the ID of the route */ public abstract void onTransferToRoute(long requestId, @NonNull String sessionId, @NonNull String routeId); /** * Called when the {@link RouteDiscoveryPreference discovery preference} has changed. *

* Whenever an application registers a {@link MediaRouter2.RouteCallback callback}, * it also provides a discovery preference to specify features of routes that it is interested * in. The media router combines all of these discovery request into a single discovery * preference and notifies each provider. *

* The provider should examine {@link RouteDiscoveryPreference#getPreferredFeatures() * preferred features} in the discovery preference to determine what kind of routes it should * try to discover and whether it should perform active or passive scans. In many cases, * the provider may be able to save power by not performing any scans when the request doesn't * have any matching route features. *

* * @param preference the new discovery preference */ public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {} /** * Updates routes of the provider and notifies the system media router service. */ public final void notifyRoutes(@NonNull Collection routes) { Objects.requireNonNull(routes, "routes must not be null"); mProviderInfo = new MediaRoute2ProviderInfo.Builder() .addRoutes(routes) .build(); schedulePublishState(); } void setCallback(IMediaRoute2ProviderServiceCallback callback) { mRemoteCallback = callback; schedulePublishState(); scheduleUpdateSessions(); } void schedulePublishState() { if (mStatePublishScheduled.compareAndSet(false, true)) { mHandler.post(this::publishState); } } private void publishState() { if (!mStatePublishScheduled.compareAndSet(true, false)) { return; } if (mRemoteCallback == null) { return; } try { mRemoteCallback.notifyProviderUpdated(mProviderInfo); } catch (RemoteException ex) { Log.w(TAG, "Failed to publish provider state.", ex); } } void scheduleUpdateSessions() { if (mSessionUpdateScheduled.compareAndSet(false, true)) { mHandler.post(this::updateSessions); } } private void updateSessions() { if (!mSessionUpdateScheduled.compareAndSet(true, false)) { return; } if (mRemoteCallback == null) { return; } List sessions; synchronized (mSessionLock) { sessions = new ArrayList<>(mSessionInfo.values()); } try { mRemoteCallback.notifySessionsUpdated(sessions); } catch (RemoteException ex) { Log.w(TAG, "Failed to notify session info changed."); } } /** * Adds a requestId in the request ID list whose max size is {@link #MAX_REQUEST_IDS_SIZE}. * When the max size is reached, the first element is removed (FIFO). */ private void addRequestId(long requestId) { synchronized (mRequestIdsLock) { if (mRequestIds.size() >= MAX_REQUEST_IDS_SIZE) { mRequestIds.removeFirst(); } mRequestIds.addLast(requestId); } } /** * Removes the given {@code requestId} from received request ID list. *

* Returns whether the list contains the {@code requestId}. These are the cases when the list * doesn't contain the given {@code requestId}: *

    *
  • This service has never received a request with the requestId.
  • *
  • {@link #notifyRequestFailed} or {@link #notifySessionCreated} already has been called * for the requestId.
  • *
*/ private boolean removeRequestId(long requestId) { synchronized (mRequestIdsLock) { return mRequestIds.removeFirstOccurrence(requestId); } } final class MediaRoute2ProviderServiceStub extends IMediaRoute2ProviderService.Stub { MediaRoute2ProviderServiceStub() { } private boolean checkCallerIsSystem() { return Binder.getCallingUid() == Process.SYSTEM_UID; } private boolean checkSessionIdIsValid(String sessionId, String description) { if (TextUtils.isEmpty(sessionId)) { Log.w(TAG, description + ": Ignoring empty sessionId from system service."); return false; } if (getSessionInfo(sessionId) == null) { Log.w(TAG, description + ": Ignoring unknown session from system service. " + "sessionId=" + sessionId); return false; } return true; } private boolean checkRouteIdIsValid(String routeId, String description) { if (TextUtils.isEmpty(routeId)) { Log.w(TAG, description + ": Ignoring empty routeId from system service."); return false; } if (mProviderInfo == null || mProviderInfo.getRoute(routeId) == null) { Log.w(TAG, description + ": Ignoring unknown route from system service. " + "routeId=" + routeId); return false; } return true; } @Override public void setCallback(IMediaRoute2ProviderServiceCallback callback) { if (!checkCallerIsSystem()) { return; } mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::setCallback, MediaRoute2ProviderService.this, callback)); } @Override public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) { if (!checkCallerIsSystem()) { return; } mHandler.sendMessage(obtainMessage( MediaRoute2ProviderService::onDiscoveryPreferenceChanged, MediaRoute2ProviderService.this, discoveryPreference)); } @Override public void setRouteVolume(long requestId, String routeId, int volume) { if (!checkCallerIsSystem()) { return; } if (!checkRouteIdIsValid(routeId, "setRouteVolume")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetRouteVolume, MediaRoute2ProviderService.this, requestId, routeId, volume)); } @Override public void requestCreateSession(long requestId, String packageName, String routeId, @Nullable Bundle requestCreateSession) { if (!checkCallerIsSystem()) { return; } if (!checkRouteIdIsValid(routeId, "requestCreateSession")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onCreateSession, MediaRoute2ProviderService.this, requestId, packageName, routeId, requestCreateSession)); } @Override public void selectRoute(long requestId, String sessionId, String routeId) { if (!checkCallerIsSystem()) { return; } if (!checkSessionIdIsValid(sessionId, "selectRoute") || !checkRouteIdIsValid(routeId, "selectRoute")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelectRoute, MediaRoute2ProviderService.this, requestId, sessionId, routeId)); } @Override public void deselectRoute(long requestId, String sessionId, String routeId) { if (!checkCallerIsSystem()) { return; } if (!checkSessionIdIsValid(sessionId, "deselectRoute") || !checkRouteIdIsValid(routeId, "deselectRoute")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onDeselectRoute, MediaRoute2ProviderService.this, requestId, sessionId, routeId)); } @Override public void transferToRoute(long requestId, String sessionId, String routeId) { if (!checkCallerIsSystem()) { return; } if (!checkSessionIdIsValid(sessionId, "transferToRoute") || !checkRouteIdIsValid(routeId, "transferToRoute")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onTransferToRoute, MediaRoute2ProviderService.this, requestId, sessionId, routeId)); } @Override public void setSessionVolume(long requestId, String sessionId, int volume) { if (!checkCallerIsSystem()) { return; } if (!checkSessionIdIsValid(sessionId, "setSessionVolume")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetSessionVolume, MediaRoute2ProviderService.this, requestId, sessionId, volume)); } @Override public void releaseSession(long requestId, String sessionId) { if (!checkCallerIsSystem()) { return; } if (!checkSessionIdIsValid(sessionId, "releaseSession")) { return; } addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession, MediaRoute2ProviderService.this, requestId, sessionId)); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy