/* * Copyright (C) 2023 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.tv.ad; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; import android.annotation.SystemService; import android.content.Context; import android.graphics.Rect; import android.media.tv.AdBuffer; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.flags.Flags; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.util.Pools; import android.util.SparseArray; import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.Surface; import android.view.View; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.Executor; /** * Central system API to the overall client-side TV AD architecture, which arbitrates interaction * between applications and AD services. */ @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW) @SystemService(Context.TV_AD_SERVICE) public final class TvAdManager { private static final String TAG = "TvAdManager"; /** * Key for package name in app link. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) */ public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name"; /** * Key for class name in app link. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) */ public static final String APP_LINK_KEY_CLASS_NAME = "class_name"; /** * Key for command type in app link command. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) */ public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type"; /** * Key for service ID in app link command. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) */ public static final String APP_LINK_KEY_SERVICE_ID = "service_id"; /** * Key for back URI in app link command. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) */ public static final String APP_LINK_KEY_BACK_URI = "back_uri"; /** * Broadcast intent action to send app command to TV app. * * @see #sendAppLinkCommand(String, Bundle) */ public static final String ACTION_APP_LINK_COMMAND = "android.media.tv.ad.action.APP_LINK_COMMAND"; /** * Intent key for TV input ID. It's used to send app command to TV app. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) * @see #ACTION_APP_LINK_COMMAND */ public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id"; /** * Intent key for TV AD service ID. It's used to send app command to TV app. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) * @see #ACTION_APP_LINK_COMMAND * @see TvAdServiceInfo#getId() */ public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id"; /** * Intent key for TV channel URI. It's used to send app command to TV app. *

Type: android.net.Uri * * @see #sendAppLinkCommand(String, Bundle) * @see #ACTION_APP_LINK_COMMAND */ public static final String INTENT_KEY_CHANNEL_URI = "channel_uri"; /** * Intent key for command type. It's used to send app command to TV app. The value of this key * could vary according to TV apps. *

Type: String * * @see #sendAppLinkCommand(String, Bundle) * @see #ACTION_APP_LINK_COMMAND */ public static final String INTENT_KEY_COMMAND_TYPE = "command_type"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef(prefix = "SESSION_DATA_TYPE_", value = { SESSION_DATA_TYPE_AD_REQUEST, SESSION_DATA_TYPE_AD_BUFFER_READY, SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST, SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST}) public @interface SessionDataType {} /** * Sends an advertisement request to be processed by the related TV input. * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) * @see SESSION_DATA_KEY_AD_REQUEST */ public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request"; /** * Notifies the advertisement buffer is ready. * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) * @see SESSION_DATA_KEY_AD_BUFFER */ public static final String SESSION_DATA_TYPE_AD_BUFFER_READY = "ad_buffer_ready"; /** * Sends request for broadcast info. * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) * @see SESSION_DATA_KEY_BROADCAST_INFO_RESQUEST */ public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request"; /** * Removes request for broadcast info. * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) * @see SESSION_DATA_KEY_BROADCAST_INFO_REQUEST_ID */ public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST = "remove_broadcast_info_request"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef(prefix = "SESSION_DATA_KEY_", value = { SESSION_DATA_KEY_AD_REQUEST, SESSION_DATA_KEY_AD_BUFFER, SESSION_DATA_KEY_BROADCAST_INFO_REQUEST, SESSION_DATA_KEY_REQUEST_ID}) public @interface SessionDataKey {} /** * An object of {@link android.media.tv.AdRequest}. * *

Type: android.media.tv.AdRequest * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) */ public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request"; /** * An object of {@link AdBuffer}. * *

Type: android.media.tv.AdBuffer * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) */ public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer"; /** * An object of {@link android.media.tv.BroadcastInfoRequest}. * *

Type: android.media.tv.BroadcastInfoRequest * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) */ public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request"; /** * The ID of {@link android.media.tv.BroadcastInfoRequest}. * *

Type: Integer * * @see TvAdService.Session#sendTvAdSessionData(String, Bundle) */ public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = false, prefix = "SESSION_STATE_", value = { SESSION_STATE_STOPPED, SESSION_STATE_RUNNING, SESSION_STATE_ERROR}) public @interface SessionState {} /** * Stopped (or not started) state of AD service session. */ public static final int SESSION_STATE_STOPPED = 1; /** * Running state of AD service session. */ public static final int SESSION_STATE_RUNNING = 2; /** * Error state of AD service session. */ public static final int SESSION_STATE_ERROR = 3; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = false, prefix = "ERROR_", value = { ERROR_NONE, ERROR_UNKNOWN, ERROR_NOT_SUPPORTED, ERROR_WEAK_SIGNAL, ERROR_RESOURCE_UNAVAILABLE, ERROR_BLOCKED, ERROR_ENCRYPTED, ERROR_UNKNOWN_CHANNEL, }) public @interface ErrorCode {} /** * No error. */ public static final int ERROR_NONE = 0; /** * Unknown error code. */ public static final int ERROR_UNKNOWN = 1; /** * Error code for an unsupported channel. */ public static final int ERROR_NOT_SUPPORTED = 2; /** * Error code for weak signal. */ public static final int ERROR_WEAK_SIGNAL = 3; /** * Error code when resource (e.g. tuner) is unavailable. */ public static final int ERROR_RESOURCE_UNAVAILABLE = 4; /** * Error code for blocked contents. */ public static final int ERROR_BLOCKED = 5; /** * Error code when the key or module is missing for the encrypted channel. */ public static final int ERROR_ENCRYPTED = 6; /** * Error code when the current channel is an unknown channel. */ public static final int ERROR_UNKNOWN_CHANNEL = 7; private final ITvAdManager mService; private final int mUserId; // A mapping from the sequence number of a session to its SessionCallbackRecord. private final SparseArray mSessionCallbackRecordMap = new SparseArray<>(); // @GuardedBy("mLock") private final List mCallbackRecords = new ArrayList<>(); // A sequence number for the next session to be created. Should be protected by a lock // {@code mSessionCallbackRecordMap}. private int mNextSeq; private final Object mLock = new Object(); private final ITvAdClient mClient; /** @hide */ public TvAdManager(ITvAdManager service, int userId) { mService = service; mUserId = userId; mClient = new ITvAdClient.Stub() { @Override public void onSessionCreated(String serviceId, IBinder token, InputChannel channel, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for " + token); return; } Session session = null; if (token != null) { session = new Session(token, channel, mService, mUserId, seq, mSessionCallbackRecordMap); } else { mSessionCallbackRecordMap.delete(seq); } record.postSessionCreated(session); } } @Override public void onSessionReleased(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); mSessionCallbackRecordMap.delete(seq); if (record == null) { Log.e(TAG, "Callback not found for seq:" + seq); return; } record.mSession.releaseInternal(); record.postSessionReleased(); } } @Override public void onLayoutSurface(int left, int top, int right, int bottom, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postLayoutSurface(left, top, right, bottom); } } @Override public void onRequestCurrentVideoBounds(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postRequestCurrentVideoBounds(); } } @Override public void onRequestCurrentChannelUri(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postRequestCurrentChannelUri(); } } @Override public void onRequestTrackInfoList(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postRequestTrackInfoList(); } } @Override public void onRequestCurrentTvInputId(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postRequestCurrentTvInputId(); } } @Override public void onRequestSigning( String id, String algorithm, String alias, byte[] data, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postRequestSigning(id, algorithm, alias, data); } } @Override public void onTvAdSessionData(String type, Bundle data, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); if (record == null) { Log.e(TAG, "Callback not found for seq " + seq); return; } record.postTvAdSessionData(type, data); } } }; ITvAdManagerCallback managerCallback = new ITvAdManagerCallback.Stub() { @Override public void onAdServiceAdded(String serviceId) { synchronized (mLock) { for (TvAdServiceCallbackRecord record : mCallbackRecords) { record.postAdServiceAdded(serviceId); } } } @Override public void onAdServiceRemoved(String serviceId) { synchronized (mLock) { for (TvAdServiceCallbackRecord record : mCallbackRecords) { record.postAdServiceRemoved(serviceId); } } } @Override public void onAdServiceUpdated(String serviceId) { synchronized (mLock) { for (TvAdServiceCallbackRecord record : mCallbackRecords) { record.postAdServiceUpdated(serviceId); } } } }; try { if (mService != null) { mService.registerCallback(managerCallback, mUserId); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the complete list of TV AD services on the system. * * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta * information. */ @NonNull public List getTvAdServiceList() { try { return mService.getTvAdServiceList(mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Creates a {@link Session} for a given TV AD service. * *

The number of sessions that can be created at the same time is limited by the capability * of the given AD service. * * @param serviceId The ID of the AD service. * @param callback A callback used to receive the created session. * @param handler A {@link Handler} that the session creation will be delivered to. * @hide */ public void createSession( @NonNull String serviceId, @NonNull String type, @NonNull final TvAdManager.SessionCallback callback, @NonNull Handler handler) { createSessionInternal(serviceId, type, callback, handler); } private void createSessionInternal(String serviceId, String type, TvAdManager.SessionCallback callback, Handler handler) { Preconditions.checkNotNull(serviceId); Preconditions.checkNotNull(type); Preconditions.checkNotNull(callback); Preconditions.checkNotNull(handler); TvAdManager.SessionCallbackRecord record = new TvAdManager.SessionCallbackRecord(callback, handler); synchronized (mSessionCallbackRecordMap) { int seq = mNextSeq++; mSessionCallbackRecordMap.put(seq, record); try { mService.createSession(mClient, serviceId, type, seq, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } } /** * Sends app link command. * * @param serviceId The ID of TV AD service which the command to be sent to. The ID can be found * in {@link TvAdServiceInfo#getId()}. * @param command The command to be sent. The command is a bundle with the following keys: *

*/ public void sendAppLinkCommand(@NonNull String serviceId, @NonNull Bundle command) { try { mService.sendAppLinkCommand(serviceId, command, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Registers a {@link TvAdServiceCallback}. * * @param callback A callback used to monitor status of the TV AD services. * @param executor A {@link Executor} that the status change will be delivered to. */ public void registerCallback( @CallbackExecutor @NonNull Executor executor, @NonNull TvAdServiceCallback callback) { Preconditions.checkNotNull(callback); Preconditions.checkNotNull(executor); synchronized (mLock) { mCallbackRecords.add(new TvAdServiceCallbackRecord(callback, executor)); } } /** * Unregisters the existing {@link TvAdServiceCallback}. * * @param callback The existing callback to remove. */ public void unregisterCallback(@NonNull final TvAdServiceCallback callback) { Preconditions.checkNotNull(callback); synchronized (mLock) { for (Iterator it = mCallbackRecords.iterator(); it.hasNext(); ) { TvAdServiceCallbackRecord record = it.next(); if (record.getCallback() == callback) { it.remove(); break; } } } } /** * The Session provides the per-session functionality of AD service. * @hide */ public static final class Session { static final int DISPATCH_IN_PROGRESS = -1; static final int DISPATCH_NOT_HANDLED = 0; static final int DISPATCH_HANDLED = 1; private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; private final ITvAdManager mService; private final int mUserId; private final int mSeq; private final SparseArray mSessionCallbackRecordMap; // For scheduling input event handling on the main thread. This also serves as a lock to // protect pending input events and the input channel. private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); private TvInputManager.Session mInputSession; private final Pools.Pool mPendingEventPool = new Pools.SimplePool<>(20); private final SparseArray mPendingEvents = new SparseArray<>(20); private TvInputEventSender mSender; private InputChannel mInputChannel; private IBinder mToken; private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId, int seq, SparseArray sessionCallbackRecordMap) { mToken = token; mInputChannel = channel; mService = service; mUserId = userId; mSeq = seq; mSessionCallbackRecordMap = sessionCallbackRecordMap; } public TvInputManager.Session getInputSession() { return mInputSession; } public void setInputSession(TvInputManager.Session inputSession) { mInputSession = inputSession; } /** * Releases this session. */ public void release() { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.releaseSession(mToken, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } releaseInternal(); } /** * Sets the {@link android.view.Surface} for this session. * * @param surface A {@link android.view.Surface} used to render AD. */ public void setSurface(Surface surface) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } // surface can be null. try { mService.setSurface(mToken, surface, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Creates a media view. Once the media view is created, {@link #relayoutMediaView} * should be called whenever the layout of its containing view is changed. * {@link #removeMediaView()} should be called to remove the media view. * Since a session can have only one media view, this method should be called only once * or it can be called again after calling {@link #removeMediaView()}. * * @param view A view for AD service. * @param frame A position of the media view. * @throws IllegalStateException if {@code view} is not attached to a window. */ void createMediaView(@NonNull View view, @NonNull Rect frame) { Preconditions.checkNotNull(view); Preconditions.checkNotNull(frame); if (view.getWindowToken() == null) { throw new IllegalStateException("view must be attached to a window"); } if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.createMediaView(mToken, view.getWindowToken(), frame, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Relayouts the current media view. * * @param frame A new position of the media view. */ void relayoutMediaView(@NonNull Rect frame) { Preconditions.checkNotNull(frame); if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.relayoutMediaView(mToken, frame, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Removes the current media view. */ void removeMediaView() { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.removeMediaView(mToken, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notifies of any structural changes (format or size) of the surface passed in * {@link #setSurface}. * * @param format The new PixelFormat of the surface. * @param width The new width of the surface. * @param height The new height of the surface. */ public void dispatchSurfaceChanged(int format, int width, int height) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private void flushPendingEventsLocked() { mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); final int count = mPendingEvents.size(); for (int i = 0; i < count; i++) { int seq = mPendingEvents.keyAt(i); Message msg = mHandler.obtainMessage( InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); msg.setAsynchronous(true); msg.sendToTarget(); } } private void releaseInternal() { mToken = null; synchronized (mHandler) { if (mInputChannel != null) { if (mSender != null) { flushPendingEventsLocked(); mSender.dispose(); mSender = null; } mInputChannel.dispose(); mInputChannel = null; } } synchronized (mSessionCallbackRecordMap) { mSessionCallbackRecordMap.delete(mSeq); } } void startAdService() { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.startAdService(mToken, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void stopAdService() { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.stopAdService(mToken, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void resetAdService() { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.resetAdService(mToken, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void sendCurrentVideoBounds(@NonNull Rect bounds) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.sendCurrentVideoBounds(mToken, bounds, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void sendCurrentChannelUri(@Nullable Uri channelUri) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.sendCurrentChannelUri(mToken, channelUri, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void sendTrackInfoList(@NonNull List tracks) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.sendTrackInfoList(mToken, tracks, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void sendCurrentTvInputId(@Nullable String inputId) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.sendCurrentTvInputId(mToken, inputId, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.sendSigningResult(mToken, signingId, result, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } void notifyError(@NonNull String errMsg, @NonNull Bundle params) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.notifyError(mToken, errMsg, params, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notifies AD service session when a new TV message is received. */ public void notifyTvMessage(int type, Bundle data) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.notifyTvMessage(mToken, type, data, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notifies data from session of linked TvInputService. */ public void notifyTvInputSessionData(String type, Bundle data) { if (mToken == null) { Log.w(TAG, "The session has been already released"); return; } try { mService.notifyTvInputSessionData(mToken, type, data, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Dispatches an input event to this session. * * @param event An {@link InputEvent} to dispatch. Cannot be {@code null}. * @param token A token used to identify the input event later in the callback. * @param callback A callback used to receive the dispatch result. Cannot be {@code null}. * @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be * {@code null}. * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will * be invoked later. * @hide */ public int dispatchInputEvent(@NonNull InputEvent event, Object token, @NonNull FinishedInputEventCallback callback, @NonNull Handler handler) { Preconditions.checkNotNull(event); Preconditions.checkNotNull(callback); Preconditions.checkNotNull(handler); synchronized (mHandler) { if (mInputChannel == null) { return DISPATCH_NOT_HANDLED; } PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); if (Looper.myLooper() == Looper.getMainLooper()) { // Already running on the main thread so we can send the event immediately. return sendInputEventOnMainLooperLocked(p); } // Post the event to the main thread. Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); msg.setAsynchronous(true); mHandler.sendMessage(msg); return DISPATCH_IN_PROGRESS; } } private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, FinishedInputEventCallback callback, Handler handler) { PendingEvent p = mPendingEventPool.acquire(); if (p == null) { p = new PendingEvent(); } p.mEvent = event; p.mEventToken = token; p.mCallback = callback; p.mEventHandler = handler; return p; } /** * Callback that is invoked when an input event that was dispatched to this session has been * finished. * * @hide */ public interface FinishedInputEventCallback { /** * Called when the dispatched input event is finished. * * @param token A token passed to {@link #dispatchInputEvent}. * @param handled {@code true} if the dispatched input event was handled properly. * {@code false} otherwise. */ void onFinishedInputEvent(Object token, boolean handled); } private final class InputEventHandler extends Handler { public static final int MSG_SEND_INPUT_EVENT = 1; public static final int MSG_TIMEOUT_INPUT_EVENT = 2; public static final int MSG_FLUSH_INPUT_EVENT = 3; InputEventHandler(Looper looper) { super(looper, null, true); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SEND_INPUT_EVENT: { sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); return; } case MSG_TIMEOUT_INPUT_EVENT: { finishedInputEvent(msg.arg1, false, true); return; } case MSG_FLUSH_INPUT_EVENT: { finishedInputEvent(msg.arg1, false, false); return; } } } } // Assumes the event has already been removed from the queue. void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { p.mHandled = handled; if (p.mEventHandler.getLooper().isCurrentThread()) { // Already running on the callback handler thread so we can send the callback // immediately. p.run(); } else { // Post the event to the callback handler thread. // In this case, the callback will be responsible for recycling the event. Message msg = Message.obtain(p.mEventHandler, p); msg.setAsynchronous(true); msg.sendToTarget(); } } // Must be called on the main looper private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { synchronized (mHandler) { int result = sendInputEventOnMainLooperLocked(p); if (result == DISPATCH_IN_PROGRESS) { return; } } invokeFinishedInputEventCallback(p, false); } private int sendInputEventOnMainLooperLocked(PendingEvent p) { if (mInputChannel != null) { if (mSender == null) { mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper()); } final InputEvent event = p.mEvent; final int seq = event.getSequenceNumber(); if (mSender.sendInputEvent(seq, event)) { mPendingEvents.put(seq, p); Message msg = mHandler.obtainMessage( InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); msg.setAsynchronous(true); mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); return DISPATCH_IN_PROGRESS; } Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + event); } return DISPATCH_NOT_HANDLED; } void finishedInputEvent(int seq, boolean handled, boolean timeout) { final PendingEvent p; synchronized (mHandler) { int index = mPendingEvents.indexOfKey(seq); if (index < 0) { return; // spurious, event already finished or timed out } p = mPendingEvents.valueAt(index); mPendingEvents.removeAt(index); if (timeout) { Log.w(TAG, "Timeout waiting for session to handle input event after " + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); } else { mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); } } invokeFinishedInputEventCallback(p, handled); } private void recyclePendingEventLocked(PendingEvent p) { p.recycle(); mPendingEventPool.release(p); } private final class TvInputEventSender extends InputEventSender { TvInputEventSender(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } @Override public void onInputEventFinished(int seq, boolean handled) { finishedInputEvent(seq, handled, false); } } private final class PendingEvent implements Runnable { public InputEvent mEvent; public Object mEventToken; public Session.FinishedInputEventCallback mCallback; public Handler mEventHandler; public boolean mHandled; public void recycle() { mEvent = null; mEventToken = null; mCallback = null; mEventHandler = null; mHandled = false; } @Override public void run() { mCallback.onFinishedInputEvent(mEventToken, mHandled); synchronized (mEventHandler) { recyclePendingEventLocked(this); } } } } /** * Interface used to receive the created session. * @hide */ public abstract static class SessionCallback { /** * This is called after {@link TvAdManager#createSession} has been processed. * * @param session A {@link TvAdManager.Session} instance created. This can be * {@code null} if the creation request failed. */ public void onSessionCreated(@Nullable Session session) { } /** * This is called when {@link TvAdManager.Session} is released. * This typically happens when the process hosting the session has crashed or been killed. * * @param session the {@link TvAdManager.Session} instance released. */ public void onSessionReleased(@NonNull Session session) { } /** * This is called when {@link TvAdService.Session#layoutSurface} is called to * change the layout of surface. * * @param session A {@link TvAdManager.Session} associated with this callback. * @param left Left position. * @param top Top position. * @param right Right position. * @param bottom Bottom position. */ public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { } /** * This is called when {@link TvAdService.Session#requestCurrentVideoBounds} is * called. * * @param session A {@link TvAdService.Session} associated with this callback. */ public void onRequestCurrentVideoBounds(Session session) { } /** * This is called when {@link TvAdService.Session#requestCurrentChannelUri} is * called. * * @param session A {@link TvAdService.Session} associated with this callback. */ public void onRequestCurrentChannelUri(Session session) { } /** * This is called when {@link TvAdService.Session#requestTrackInfoList} is * called. * * @param session A {@link TvAdService.Session} associated with this callback. */ public void onRequestTrackInfoList(Session session) { } /** * This is called when {@link TvAdService.Session#requestCurrentTvInputId} is * called. * * @param session A {@link TvAdService.Session} associated with this callback. */ public void onRequestCurrentTvInputId(Session session) { } /** * This is called when * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} is * called. * * @param session A {@link TvAdService.Session} associated with this callback. * @param signingId the ID to identify the request. * @param algorithm the standard name of the signature algorithm requested, such as * MD5withRSA, SHA256withDSA, etc. * @param alias the alias of the corresponding {@link java.security.KeyStore}. * @param data the original bytes to be signed. */ public void onRequestSigning( Session session, String signingId, String algorithm, String alias, byte[] data) { } } /** * Callback used to monitor status of the TV advertisement service. */ public abstract static class TvAdServiceCallback { /** * This is called when a TV AD service is added to the system. * *

Normally it happens when the user installs a new TV AD service package that implements * {@link TvAdService} interface. * * @param serviceId The ID of the TV AD service. */ public void onAdServiceAdded(@NonNull String serviceId) { } /** * This is called when a TV AD service is removed from the system. * *

Normally it happens when the user uninstalls the previously installed TV AD service * package. * * @param serviceId The ID of the TV AD service. */ public void onAdServiceRemoved(@NonNull String serviceId) { } /** * This is called when a TV AD service is updated on the system. * *

Normally it happens when a previously installed TV AD service package is re-installed * or a newer version of the package exists becomes available/unavailable. * * @param serviceId The ID of the TV AD service. */ public void onAdServiceUpdated(@NonNull String serviceId) { } } private static final class SessionCallbackRecord { private final SessionCallback mSessionCallback; private final Handler mHandler; private Session mSession; SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) { mSessionCallback = sessionCallback; mHandler = handler; } void postSessionCreated(final Session session) { mSession = session; mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onSessionCreated(session); } }); } void postSessionReleased() { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onSessionReleased(mSession); } }); } void postLayoutSurface(final int left, final int top, final int right, final int bottom) { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom); } }); } void postRequestCurrentVideoBounds() { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onRequestCurrentVideoBounds(mSession); } }); } void postRequestCurrentChannelUri() { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onRequestCurrentChannelUri(mSession); } }); } void postRequestTrackInfoList() { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onRequestTrackInfoList(mSession); } }); } void postRequestCurrentTvInputId() { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onRequestCurrentTvInputId(mSession); } }); } void postRequestSigning(String id, String algorithm, String alias, byte[] data) { mHandler.post(new Runnable() { @Override public void run() { mSessionCallback.onRequestSigning(mSession, id, algorithm, alias, data); } }); } void postTvAdSessionData(String type, Bundle data) { mHandler.post(new Runnable() { @Override public void run() { if (mSession.getInputSession() != null) { mSession.getInputSession().notifyTvAdSessionData(type, data); } } }); } } private static final class TvAdServiceCallbackRecord { private final TvAdServiceCallback mCallback; private final Executor mExecutor; TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) { mCallback = callback; mExecutor = executor; } public TvAdServiceCallback getCallback() { return mCallback; } public void postAdServiceAdded(final String serviceId) { mExecutor.execute(new Runnable() { @Override public void run() { mCallback.onAdServiceAdded(serviceId); } }); } public void postAdServiceRemoved(final String serviceId) { mExecutor.execute(new Runnable() { @Override public void run() { mCallback.onAdServiceRemoved(serviceId); } }); } public void postAdServiceUpdated(final String serviceId) { mExecutor.execute(new Runnable() { @Override public void run() { mCallback.onAdServiceUpdated(serviceId); } }); } } }