997 lines
37 KiB
Java
997 lines
37 KiB
Java
/*
|
|
* 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.CallSuper;
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.MainThread;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.Px;
|
|
import android.annotation.SdkConstant;
|
|
import android.annotation.SuppressLint;
|
|
import android.app.ActivityManager;
|
|
import android.app.Service;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.PixelFormat;
|
|
import android.graphics.Rect;
|
|
import android.media.tv.TvInputManager;
|
|
import android.media.tv.TvTrackInfo;
|
|
import android.media.tv.TvView;
|
|
import android.media.tv.flags.Flags;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Message;
|
|
import android.os.Process;
|
|
import android.os.RemoteCallbackList;
|
|
import android.os.RemoteException;
|
|
import android.util.Log;
|
|
import android.view.Gravity;
|
|
import android.view.InputChannel;
|
|
import android.view.InputDevice;
|
|
import android.view.InputEvent;
|
|
import android.view.InputEventReceiver;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.Surface;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.widget.FrameLayout;
|
|
|
|
import com.android.internal.os.SomeArgs;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* The TvAdService class represents a TV client-side advertisement service.
|
|
*/
|
|
@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
|
|
public abstract class TvAdService extends Service {
|
|
private static final boolean DEBUG = false;
|
|
private static final String TAG = "TvAdService";
|
|
|
|
private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000;
|
|
|
|
/**
|
|
* Name under which a TvAdService component publishes information about itself. This meta-data
|
|
* must reference an XML resource containing an
|
|
* <code><{@link android.R.styleable#TvAdService tv-ad-service}></code> tag.
|
|
*/
|
|
public static final String SERVICE_META_DATA = "android.media.tv.ad.service";
|
|
|
|
/**
|
|
* This is the interface name that a service implementing a TV AD service should
|
|
* say that it supports -- that is, this is the action it uses for its intent filter. To be
|
|
* supported, the service must also require the
|
|
* android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other
|
|
* applications cannot abuse it.
|
|
*/
|
|
@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
|
|
public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService";
|
|
|
|
private final Handler mServiceHandler = new ServiceHandler();
|
|
private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>();
|
|
|
|
@Override
|
|
@Nullable
|
|
public final IBinder onBind(@Nullable Intent intent) {
|
|
ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() {
|
|
@Override
|
|
public void registerCallback(ITvAdServiceCallback cb) {
|
|
if (cb != null) {
|
|
mCallbacks.register(cb);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void unregisterCallback(ITvAdServiceCallback cb) {
|
|
if (cb != null) {
|
|
mCallbacks.unregister(cb);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void createSession(InputChannel channel, ITvAdSessionCallback cb,
|
|
String serviceId, String type) {
|
|
if (cb == null) {
|
|
return;
|
|
}
|
|
SomeArgs args = SomeArgs.obtain();
|
|
args.arg1 = channel;
|
|
args.arg2 = cb;
|
|
args.arg3 = serviceId;
|
|
args.arg4 = type;
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args)
|
|
.sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void sendAppLinkCommand(Bundle command) {
|
|
onAppLinkCommand(command);
|
|
}
|
|
};
|
|
return tvAdServiceBinder;
|
|
}
|
|
|
|
/**
|
|
* Called when app link command is received.
|
|
*
|
|
* @see TvAdManager#sendAppLinkCommand(String, Bundle)
|
|
*/
|
|
public void onAppLinkCommand(@NonNull Bundle command) {
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a concrete implementation of {@link Session}.
|
|
*
|
|
* <p>May return {@code null} if this TV AD service fails to create a session for some
|
|
* reason.
|
|
*
|
|
* @param serviceId The ID of the TV AD associated with the session.
|
|
* @param type The type of the TV AD associated with the session.
|
|
*/
|
|
@Nullable
|
|
public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type);
|
|
|
|
/**
|
|
* Base class for derived classes to implement to provide a TV AD session.
|
|
*/
|
|
public abstract static class Session implements KeyEvent.Callback {
|
|
private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
|
|
|
|
private final Object mLock = new Object();
|
|
// @GuardedBy("mLock")
|
|
private ITvAdSessionCallback mSessionCallback;
|
|
// @GuardedBy("mLock")
|
|
private final List<Runnable> mPendingActions = new ArrayList<>();
|
|
private final Context mContext;
|
|
final Handler mHandler;
|
|
private final WindowManager mWindowManager;
|
|
private WindowManager.LayoutParams mWindowParams;
|
|
private Surface mSurface;
|
|
private FrameLayout mMediaViewContainer;
|
|
private View mMediaView;
|
|
private MediaViewCleanUpTask mMediaViewCleanUpTask;
|
|
private boolean mMediaViewEnabled;
|
|
private IBinder mWindowToken;
|
|
private Rect mMediaFrame;
|
|
|
|
|
|
/**
|
|
* Creates a new Session.
|
|
*
|
|
* @param context The context of the application
|
|
*/
|
|
public Session(@NonNull Context context) {
|
|
mContext = context;
|
|
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
|
mHandler = new Handler(context.getMainLooper());
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the media view.
|
|
*
|
|
* <p>By default, the media view is disabled. Must be called explicitly after the
|
|
* session is created to enable the media view.
|
|
*
|
|
* <p>The TV AD service can disable its media view when needed.
|
|
*
|
|
* @param enable {@code true} if you want to enable the media view. {@code false}
|
|
* otherwise.
|
|
*/
|
|
@CallSuper
|
|
public void setMediaViewEnabled(final boolean enable) {
|
|
mHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (enable == mMediaViewEnabled) {
|
|
return;
|
|
}
|
|
mMediaViewEnabled = enable;
|
|
if (enable) {
|
|
if (mWindowToken != null) {
|
|
createMediaView(mWindowToken, mMediaFrame);
|
|
}
|
|
} else {
|
|
removeMediaView(false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if media view is enabled, {@code false} otherwise.
|
|
*
|
|
* @see #setMediaViewEnabled(boolean)
|
|
*/
|
|
public boolean isMediaViewEnabled() {
|
|
return mMediaViewEnabled;
|
|
}
|
|
|
|
/**
|
|
* Releases TvAdService session.
|
|
*/
|
|
public abstract void onRelease();
|
|
|
|
void release() {
|
|
onRelease();
|
|
if (mSurface != null) {
|
|
mSurface.release();
|
|
mSurface = null;
|
|
}
|
|
synchronized (mLock) {
|
|
mSessionCallback = null;
|
|
mPendingActions.clear();
|
|
}
|
|
// Removes the media view lastly so that any hanging on the main thread can be handled
|
|
// in {@link #scheduleMediaViewCleanup}.
|
|
removeMediaView(true);
|
|
}
|
|
|
|
/**
|
|
* Starts TvAdService session.
|
|
*/
|
|
public void onStartAdService() {
|
|
}
|
|
|
|
/**
|
|
* Stops TvAdService session.
|
|
*/
|
|
public void onStopAdService() {
|
|
}
|
|
|
|
/**
|
|
* Resets TvAdService session.
|
|
*/
|
|
public void onResetAdService() {
|
|
}
|
|
|
|
void startAdService() {
|
|
onStartAdService();
|
|
}
|
|
|
|
void stopAdService() {
|
|
onStopAdService();
|
|
}
|
|
|
|
void resetAdService() {
|
|
onResetAdService();
|
|
}
|
|
|
|
/**
|
|
* Requests the bounds of the current video.
|
|
*/
|
|
@CallSuper
|
|
public void requestCurrentVideoBounds() {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "requestCurrentVideoBounds");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onRequestCurrentVideoBounds();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in requestCurrentVideoBounds", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Requests the URI of the current channel.
|
|
*/
|
|
@CallSuper
|
|
public void requestCurrentChannelUri() {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "requestCurrentChannelUri");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onRequestCurrentChannelUri();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in requestCurrentChannelUri", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Requests the list of {@link TvTrackInfo}.
|
|
*/
|
|
@CallSuper
|
|
public void requestTrackInfoList() {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "requestTrackInfoList");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onRequestTrackInfoList();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in requestTrackInfoList", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Requests current TV input ID.
|
|
*
|
|
* @see android.media.tv.TvInputInfo
|
|
*/
|
|
@CallSuper
|
|
public void requestCurrentTvInputId() {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "requestCurrentTvInputId");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onRequestCurrentTvInputId();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in requestCurrentTvInputId", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Requests signing of the given data.
|
|
*
|
|
* <p>This is used when the corresponding server of the AD service app requires signing
|
|
* during handshaking, and the service doesn't have the built-in private key. The private
|
|
* key is provided by the content providers and pre-built in the related app, such as TV
|
|
* app.
|
|
*
|
|
* @param signingId the ID to identify the request. When a result is received, this ID can
|
|
* be used to correlate the result with the request.
|
|
* @param algorithm the standard name of the signature algorithm requested, such as
|
|
* MD5withRSA, SHA256withDSA, etc. The name is from standards like
|
|
* FIPS PUB 186-4 and PKCS #1.
|
|
* @param alias the alias of the corresponding {@link java.security.KeyStore}.
|
|
* @param data the original bytes to be signed.
|
|
*
|
|
* @see #onSigningResult(String, byte[])
|
|
*/
|
|
@CallSuper
|
|
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
|
|
@NonNull String alias, @NonNull byte[] data) {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "requestSigning");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onRequestSigning(signingId, algorithm, alias, data);
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in requestSigning", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, @Nullable KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyLongPress(int keyCode, @Nullable KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyMultiple(int keyCode, int count, @Nullable KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, @Nullable KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle touch screen motion events on the current session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onTouchEvent
|
|
*/
|
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle trackball events on the current session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onTrackballEvent
|
|
*/
|
|
public boolean onTrackballEvent(@NonNull MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle generic motion events on the current session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onGenericMotionEvent
|
|
*/
|
|
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
|
|
* is relative to the overlay view that sits on top of this surface.
|
|
*
|
|
* @param left Left position in pixels, relative to the overlay view.
|
|
* @param top Top position in pixels, relative to the overlay view.
|
|
* @param right Right position in pixels, relative to the overlay view.
|
|
* @param bottom Bottom position in pixels, relative to the overlay view.
|
|
*
|
|
*/
|
|
@CallSuper
|
|
public void layoutSurface(final int left, final int top, final int right,
|
|
final int bottom) {
|
|
if (left > right || top > bottom) {
|
|
throw new IllegalArgumentException("Invalid parameter");
|
|
}
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top
|
|
+ ", r=" + right + ", b=" + bottom + ",)");
|
|
}
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onLayoutSurface(left, top, right, bottom);
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in layoutSurface", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when the application sets the surface.
|
|
*
|
|
* <p>The TV AD service should render AD UI onto the given surface. When called with
|
|
* {@code null}, the AD service should immediately free any references to the currently set
|
|
* surface and stop using it.
|
|
*
|
|
* @param surface The surface to be used for AD UI rendering. Can be {@code null}.
|
|
* @return {@code true} if the surface was set successfully, {@code false} otherwise.
|
|
*/
|
|
public abstract boolean onSetSurface(@Nullable Surface surface);
|
|
|
|
/**
|
|
* Called after any structural changes (format or size) have been made to the surface passed
|
|
* in {@link #onSetSurface}. This method is always called at least once, after
|
|
* {@link #onSetSurface} is called with non-null surface.
|
|
*
|
|
* @param format The new {@link PixelFormat} of the surface.
|
|
* @param width The new width of the surface.
|
|
* @param height The new height of the surface.
|
|
*/
|
|
public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) {
|
|
}
|
|
|
|
/**
|
|
* Receives current video bounds.
|
|
*
|
|
* @param bounds the rectangle area for rendering the current video.
|
|
*/
|
|
public void onCurrentVideoBounds(@NonNull Rect bounds) {
|
|
}
|
|
|
|
/**
|
|
* Receives current channel URI.
|
|
*/
|
|
public void onCurrentChannelUri(@Nullable Uri channelUri) {
|
|
}
|
|
|
|
/**
|
|
* Receives track list.
|
|
*/
|
|
public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
|
|
}
|
|
|
|
/**
|
|
* Receives current TV input ID.
|
|
*/
|
|
public void onCurrentTvInputId(@Nullable String inputId) {
|
|
}
|
|
|
|
/**
|
|
* Receives signing result.
|
|
*
|
|
* @param signingId the ID to identify the request. It's the same as the corresponding ID in
|
|
* {@link Session#requestSigning(String, String, String, byte[])}
|
|
* @param result the signed result.
|
|
*
|
|
* @see #requestSigning(String, String, String, byte[])
|
|
*/
|
|
public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) {
|
|
}
|
|
|
|
/**
|
|
* Called when the application sends information of an error.
|
|
*
|
|
* @param errMsg the message of the error.
|
|
* @param params additional parameters of the error. For example, the signingId of {@link
|
|
* TvAdView.TvAdCallback#onRequestSigning(String, String, String, String, byte[])}
|
|
* can be included to identify the related signing request, and the method name
|
|
* "onRequestSigning" can also be added to the params.
|
|
*
|
|
* @see TvAdView#ERROR_KEY_METHOD_NAME
|
|
*/
|
|
public void onError(@NonNull String errMsg, @NonNull Bundle params) {
|
|
}
|
|
|
|
/**
|
|
* Called when a TV message is received
|
|
*
|
|
* @param type The type of message received, such as
|
|
* {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
|
|
* @param data The raw data of the message. The bundle keys are:
|
|
* {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
|
|
* {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID},
|
|
* {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
|
|
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
|
|
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
|
|
* how to parse this data.
|
|
*/
|
|
public void onTvMessage(@TvInputManager.TvMessageType int type,
|
|
@NonNull Bundle data) {
|
|
}
|
|
|
|
/**
|
|
* Called when data from the linked {@link android.media.tv.TvInputService} is received.
|
|
*
|
|
* @param type the type of the data
|
|
* @param data a bundle contains the data received
|
|
* @see android.media.tv.TvInputService.Session#sendTvInputSessionData(String, Bundle)
|
|
* @see android.media.tv.ad.TvAdView#setTvView(TvView)
|
|
*/
|
|
public void onTvInputSessionData(
|
|
@NonNull @TvInputManager.SessionDataType String type, @NonNull Bundle data) {
|
|
}
|
|
|
|
/**
|
|
* Called when the size of the media view is changed by the application.
|
|
*
|
|
* <p>This is always called at least once when the session is created regardless of whether
|
|
* the media view is enabled or not. The media view container size is the same as the
|
|
* containing {@link TvAdView}. Note that the size of the underlying surface can
|
|
* be different if the surface was changed by calling {@link #layoutSurface}.
|
|
*
|
|
* @param width The width of the media view, in pixels.
|
|
* @param height The height of the media view, in pixels.
|
|
*/
|
|
public void onMediaViewSizeChanged(@Px int width, @Px int height) {
|
|
}
|
|
|
|
/**
|
|
* Called when the application requests to create a media view. Each session
|
|
* implementation can override this method and return its own view.
|
|
*
|
|
* @return a view attached to the media window. {@code null} if no media view is created.
|
|
*/
|
|
@Nullable
|
|
public View onCreateMediaView() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sends data related to this session to corresponding linked
|
|
* {@link android.media.tv.TvInputService} object via TvView.
|
|
*
|
|
* @param type data type
|
|
* @param data the related data values
|
|
* @see TvAdView#setTvView(TvView)
|
|
*/
|
|
public void sendTvAdSessionData(
|
|
@NonNull @TvAdManager.SessionDataType String type, @NonNull Bundle data) {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "sendTvAdSessionData");
|
|
if (mSessionCallback != null) {
|
|
mSessionCallback.onTvAdSessionData(type, data);
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in sendTvAdSessionData", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notifies when the session state is changed.
|
|
*
|
|
* @param state the current session state.
|
|
* @param err the error code for error state. {@link TvAdManager#ERROR_NONE} is
|
|
* used when the state is not {@link TvAdManager#SESSION_STATE_ERROR}.
|
|
*/
|
|
@CallSuper
|
|
public void notifySessionStateChanged(
|
|
@TvAdManager.SessionState int state,
|
|
@TvAdManager.ErrorCode int err) {
|
|
executeOrPostRunnableOnMainThread(new Runnable() {
|
|
@MainThread
|
|
@Override
|
|
public void run() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "notifySessionStateChanged (state="
|
|
+ state + "; err=" + err + ")");
|
|
}
|
|
// TODO: handle session callback
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Takes care of dispatching incoming input events and tells whether the event was handled.
|
|
*/
|
|
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
|
|
if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
|
|
if (event instanceof KeyEvent) {
|
|
KeyEvent keyEvent = (KeyEvent) event;
|
|
if (keyEvent.dispatch(this, mDispatcherState, this)) {
|
|
return TvAdManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
|
|
// TODO: special handlings of navigation keys and media keys
|
|
} else if (event instanceof MotionEvent) {
|
|
MotionEvent motionEvent = (MotionEvent) event;
|
|
final int source = motionEvent.getSource();
|
|
if (motionEvent.isTouchEvent()) {
|
|
if (onTouchEvent(motionEvent)) {
|
|
return TvAdManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
|
|
if (onTrackballEvent(motionEvent)) {
|
|
return TvAdManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
} else {
|
|
if (onGenericMotionEvent(motionEvent)) {
|
|
return TvAdManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
// TODO: handle overlay view
|
|
return TvAdManager.Session.DISPATCH_NOT_HANDLED;
|
|
}
|
|
|
|
|
|
private void initialize(ITvAdSessionCallback callback) {
|
|
synchronized (mLock) {
|
|
mSessionCallback = callback;
|
|
for (Runnable runnable : mPendingActions) {
|
|
runnable.run();
|
|
}
|
|
mPendingActions.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSetSurface}.
|
|
*/
|
|
void setSurface(Surface surface) {
|
|
onSetSurface(surface);
|
|
if (mSurface != null) {
|
|
mSurface.release();
|
|
}
|
|
mSurface = surface;
|
|
// TODO: Handle failure.
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSurfaceChanged}.
|
|
*/
|
|
void dispatchSurfaceChanged(int format, int width, int height) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
|
|
+ ", height=" + height + ")");
|
|
}
|
|
onSurfaceChanged(format, width, height);
|
|
}
|
|
|
|
void sendCurrentVideoBounds(@NonNull Rect bounds) {
|
|
onCurrentVideoBounds(bounds);
|
|
}
|
|
|
|
void sendCurrentChannelUri(@Nullable Uri channelUri) {
|
|
onCurrentChannelUri(channelUri);
|
|
}
|
|
|
|
void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
|
|
onTrackInfoList(tracks);
|
|
}
|
|
|
|
void sendCurrentTvInputId(@Nullable String inputId) {
|
|
onCurrentTvInputId(inputId);
|
|
}
|
|
|
|
void sendSigningResult(String signingId, byte[] result) {
|
|
onSigningResult(signingId, result);
|
|
}
|
|
|
|
void notifyError(String errMsg, Bundle params) {
|
|
onError(errMsg, params);
|
|
}
|
|
|
|
void notifyTvMessage(int type, Bundle data) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "notifyTvMessage (type=" + type + ", data= " + data + ")");
|
|
}
|
|
onTvMessage(type, data);
|
|
}
|
|
|
|
void notifyTvInputSessionData(String type, Bundle data) {
|
|
onTvInputSessionData(type, data);
|
|
}
|
|
|
|
private void executeOrPostRunnableOnMainThread(Runnable action) {
|
|
synchronized (mLock) {
|
|
if (mSessionCallback == null) {
|
|
// The session is not initialized yet.
|
|
mPendingActions.add(action);
|
|
} else {
|
|
if (mHandler.getLooper().isCurrentThread()) {
|
|
action.run();
|
|
} else {
|
|
// Posts the runnable if this is not called from the main thread
|
|
mHandler.post(action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a media view. This calls {@link #onCreateMediaView} to get a view to attach
|
|
* to the media window.
|
|
*
|
|
* @param windowToken A window token of the application.
|
|
* @param frame A position of the media view.
|
|
*/
|
|
void createMediaView(IBinder windowToken, Rect frame) {
|
|
if (mMediaViewContainer != null) {
|
|
removeMediaView(false);
|
|
}
|
|
if (DEBUG) Log.d(TAG, "create media view(" + frame + ")");
|
|
mWindowToken = windowToken;
|
|
mMediaFrame = frame;
|
|
onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
|
|
if (!mMediaViewEnabled) {
|
|
return;
|
|
}
|
|
mMediaView = onCreateMediaView();
|
|
if (mMediaView == null) {
|
|
return;
|
|
}
|
|
if (mMediaViewCleanUpTask != null) {
|
|
mMediaViewCleanUpTask.cancel(true);
|
|
mMediaViewCleanUpTask = null;
|
|
}
|
|
// Creates a container view to check hanging on the media view detaching.
|
|
// Adding/removing the media view to/from the container make the view attach/detach
|
|
// logic run on the main thread.
|
|
mMediaViewContainer = new FrameLayout(mContext.getApplicationContext());
|
|
mMediaViewContainer.addView(mMediaView);
|
|
|
|
int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
|
|
// We make the overlay view non-focusable and non-touchable so that
|
|
// the application that owns the window token can decide whether to consume or
|
|
// dispatch the input events.
|
|
int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
|
|
if (ActivityManager.isHighEndGfx()) {
|
|
flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
|
|
}
|
|
mWindowParams = new WindowManager.LayoutParams(
|
|
frame.right - frame.left, frame.bottom - frame.top,
|
|
frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT);
|
|
mWindowParams.privateFlags |=
|
|
WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
|
|
mWindowParams.gravity = Gravity.START | Gravity.TOP;
|
|
mWindowParams.token = windowToken;
|
|
mWindowManager.addView(mMediaViewContainer, mWindowParams);
|
|
}
|
|
|
|
/**
|
|
* Relayouts the current media view.
|
|
*
|
|
* @param frame A new position of the media view.
|
|
*/
|
|
void relayoutMediaView(Rect frame) {
|
|
if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")");
|
|
if (mMediaFrame == null || mMediaFrame.width() != frame.width()
|
|
|| mMediaFrame.height() != frame.height()) {
|
|
// Note: relayoutMediaView is called whenever TvAdView's layout is
|
|
// changed regardless of setMediaViewEnabled.
|
|
onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
|
|
}
|
|
mMediaFrame = frame;
|
|
if (!mMediaViewEnabled || mMediaViewContainer == null) {
|
|
return;
|
|
}
|
|
mWindowParams.x = frame.left;
|
|
mWindowParams.y = frame.top;
|
|
mWindowParams.width = frame.right - frame.left;
|
|
mWindowParams.height = frame.bottom - frame.top;
|
|
mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams);
|
|
}
|
|
|
|
/**
|
|
* Removes the current media view.
|
|
*/
|
|
void removeMediaView(boolean clearWindowToken) {
|
|
if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")");
|
|
if (clearWindowToken) {
|
|
mWindowToken = null;
|
|
mMediaFrame = null;
|
|
}
|
|
if (mMediaViewContainer != null) {
|
|
// Removes the media view from the view hierarchy in advance so that it can be
|
|
// cleaned up in the {@link MediaViewCleanUpTask} if the remove process is
|
|
// hanging.
|
|
mMediaViewContainer.removeView(mMediaView);
|
|
mMediaView = null;
|
|
mWindowManager.removeView(mMediaViewContainer);
|
|
mMediaViewContainer = null;
|
|
mWindowParams = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedules a task which checks whether the media view is detached and kills the process
|
|
* if it is not. Note that this method is expected to be called in a non-main thread.
|
|
*/
|
|
void scheduleMediaViewCleanup() {
|
|
View mediaViewParent = mMediaViewContainer;
|
|
if (mediaViewParent != null) {
|
|
mMediaViewCleanUpTask = new MediaViewCleanUpTask();
|
|
mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
|
mediaViewParent);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class MediaViewCleanUpTask extends AsyncTask<View, Void, Void> {
|
|
@Override
|
|
protected Void doInBackground(View... views) {
|
|
View mediaViewParent = views[0];
|
|
try {
|
|
Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS);
|
|
} catch (InterruptedException e) {
|
|
return null;
|
|
}
|
|
if (isCancelled()) {
|
|
return null;
|
|
}
|
|
if (mediaViewParent.isAttachedToWindow()) {
|
|
Log.e(TAG, "Time out on releasing media view. Killing "
|
|
+ mediaViewParent.getContext().getPackageName());
|
|
android.os.Process.killProcess(Process.myPid());
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
@SuppressLint("HandlerLeak")
|
|
private final class ServiceHandler extends Handler {
|
|
private static final int DO_CREATE_SESSION = 1;
|
|
private static final int DO_NOTIFY_SESSION_CREATED = 2;
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
switch (msg.what) {
|
|
case DO_CREATE_SESSION: {
|
|
SomeArgs args = (SomeArgs) msg.obj;
|
|
InputChannel channel = (InputChannel) args.arg1;
|
|
ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2;
|
|
String serviceId = (String) args.arg3;
|
|
String type = (String) args.arg4;
|
|
args.recycle();
|
|
TvAdService.Session sessionImpl = onCreateSession(serviceId, type);
|
|
if (sessionImpl == null) {
|
|
try {
|
|
// Failed to create a session.
|
|
cb.onSessionCreated(null);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "error in onSessionCreated", e);
|
|
}
|
|
return;
|
|
}
|
|
ITvAdSession stub =
|
|
new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel);
|
|
|
|
SomeArgs someArgs = SomeArgs.obtain();
|
|
someArgs.arg1 = sessionImpl;
|
|
someArgs.arg2 = stub;
|
|
someArgs.arg3 = cb;
|
|
mServiceHandler.obtainMessage(
|
|
DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget();
|
|
return;
|
|
}
|
|
case DO_NOTIFY_SESSION_CREATED: {
|
|
SomeArgs args = (SomeArgs) msg.obj;
|
|
Session sessionImpl = (Session) args.arg1;
|
|
ITvAdSession stub = (ITvAdSession) args.arg2;
|
|
ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3;
|
|
try {
|
|
cb.onSessionCreated(stub);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "error in onSessionCreated", e);
|
|
}
|
|
if (sessionImpl != null) {
|
|
sessionImpl.initialize(cb);
|
|
}
|
|
args.recycle();
|
|
return;
|
|
}
|
|
default: {
|
|
Log.w(TAG, "Unhandled message code: " + msg.what);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|