1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.tv;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SystemApi;
24 import android.content.Context;
25 import android.content.pm.PackageManager;
26 import android.graphics.Canvas;
27 import android.graphics.PorterDuff;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.Region;
31 import android.media.PlaybackParams;
32 import android.media.tv.TvInputManager.Session;
33 import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
34 import android.media.tv.TvInputManager.SessionCallback;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.text.TextUtils;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.util.Pair;
42 import android.view.InputEvent;
43 import android.view.KeyEvent;
44 import android.view.MotionEvent;
45 import android.view.Surface;
46 import android.view.SurfaceHolder;
47 import android.view.SurfaceView;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.ViewRootImpl;
51 
52 import java.lang.ref.WeakReference;
53 import java.util.ArrayDeque;
54 import java.util.List;
55 import java.util.Queue;
56 
57 /**
58  * Displays TV contents. The TvView class provides a high level interface for applications to show
59  * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of
60  * TV inputs available on the system can be obtained by calling
61  * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.)
62  *
63  * <p>Once the application supplies the URI for a specific TV channel to {@link #tune}
64  * method, it takes care of underlying service binding (and unbinding if the current TvView is
65  * already bound to a service) and automatically allocates/deallocates resources needed. In addition
66  * to a few essential methods to control how the contents are presented, it also provides a way to
67  * dispatch input events to the connected TvInputService in order to enable custom key actions for
68  * the TV input.
69  */
70 public class TvView extends ViewGroup {
71     private static final String TAG = "TvView";
72     private static final boolean DEBUG = false;
73 
74     private static final int ZORDER_MEDIA = 0;
75     private static final int ZORDER_MEDIA_OVERLAY = 1;
76     private static final int ZORDER_ON_TOP = 2;
77 
78     private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null);
79 
80     private static final Object sMainTvViewLock = new Object();
81     private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
82 
83     private final Handler mHandler = new Handler();
84     private Session mSession;
85     private SurfaceView mSurfaceView;
86     private Surface mSurface;
87     private boolean mOverlayViewCreated;
88     private Rect mOverlayViewFrame;
89     private final TvInputManager mTvInputManager;
90     private MySessionCallback mSessionCallback;
91     private TvInputCallback mCallback;
92     private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
93     private Float mStreamVolume;
94     private Boolean mCaptionEnabled;
95     private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>();
96 
97     private boolean mSurfaceChanged;
98     private int mSurfaceFormat;
99     private int mSurfaceWidth;
100     private int mSurfaceHeight;
101     private final AttributeSet mAttrs;
102     private final int mDefStyleAttr;
103     private int mWindowZOrder;
104     private boolean mUseRequestedSurfaceLayout;
105     private int mSurfaceViewLeft;
106     private int mSurfaceViewRight;
107     private int mSurfaceViewTop;
108     private int mSurfaceViewBottom;
109     private TimeShiftPositionCallback mTimeShiftPositionCallback;
110 
111     private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
112         @Override
113         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
114             if (DEBUG) {
115                 Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width="
116                     + width + ", height=" + height + ")");
117             }
118             mSurfaceFormat = format;
119             mSurfaceWidth = width;
120             mSurfaceHeight = height;
121             mSurfaceChanged = true;
122             dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
123         }
124 
125         @Override
126         public void surfaceCreated(SurfaceHolder holder) {
127             mSurface = holder.getSurface();
128             setSessionSurface(mSurface);
129         }
130 
131         @Override
132         public void surfaceDestroyed(SurfaceHolder holder) {
133             mSurface = null;
134             mSurfaceChanged = false;
135             setSessionSurface(null);
136         }
137     };
138 
139     private final FinishedInputEventCallback mFinishedInputEventCallback =
140             new FinishedInputEventCallback() {
141         @Override
142         public void onFinishedInputEvent(Object token, boolean handled) {
143             if (DEBUG) {
144                 Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
145             }
146             if (handled) {
147                 return;
148             }
149             // TODO: Re-order unhandled events.
150             InputEvent event = (InputEvent) token;
151             if (dispatchUnhandledInputEvent(event)) {
152                 return;
153             }
154             ViewRootImpl viewRootImpl = getViewRootImpl();
155             if (viewRootImpl != null) {
156                 viewRootImpl.dispatchUnhandledInputEvent(event);
157             }
158         }
159     };
160 
TvView(Context context)161     public TvView(Context context) {
162         this(context, null, 0);
163     }
164 
TvView(Context context, AttributeSet attrs)165     public TvView(Context context, AttributeSet attrs) {
166         this(context, attrs, 0);
167     }
168 
TvView(Context context, AttributeSet attrs, int defStyleAttr)169     public TvView(Context context, AttributeSet attrs, int defStyleAttr) {
170         super(context, attrs, defStyleAttr);
171         mAttrs = attrs;
172         mDefStyleAttr = defStyleAttr;
173         resetSurfaceView();
174         mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE);
175     }
176 
177     /**
178      * Sets the callback to be invoked when an event is dispatched to this TvView.
179      *
180      * @param callback The callback to receive events. A value of {@code null} removes the existing
181      *            callback.
182      */
setCallback(@ullable TvInputCallback callback)183     public void setCallback(@Nullable TvInputCallback callback) {
184         mCallback = callback;
185     }
186 
187     /**
188      * Sets this as the main {@link TvView}.
189      *
190      * <p>The main {@link TvView} is a {@link TvView} whose corresponding TV input determines the
191      * HDMI-CEC active source device. For an HDMI port input, one of source devices that is
192      * connected to that HDMI port becomes the active source. For an HDMI-CEC logical device input,
193      * the corresponding HDMI-CEC logical device becomes the active source. For any non-HDMI input
194      * (including the tuner, composite, S-Video, etc.), the internal device (= TV itself) becomes
195      * the active source.
196      *
197      * <p>First tuned {@link TvView} becomes main automatically, and keeps to be main until either
198      * {@link #reset} is called for the main {@link TvView} or {@code setMain()} is called for other
199      * {@link TvView}.
200      * @hide
201      */
202     @SystemApi
203     @RequiresPermission(android.Manifest.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE)
setMain()204     public void setMain() {
205         synchronized (sMainTvViewLock) {
206             sMainTvView = new WeakReference<>(this);
207             if (hasWindowFocus() && mSession != null) {
208                 mSession.setMain();
209             }
210         }
211     }
212 
213     /**
214      * Controls whether the TvView's surface is placed on top of another regular surface view in the
215      * window (but still behind the window itself).
216      * This is typically used to place overlays on top of an underlying TvView.
217      *
218      * <p>Note that this must be set before the TvView's containing window is attached to the
219      * window manager.
220      *
221      * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
222      *
223      * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false}
224      *            otherwise.
225      */
setZOrderMediaOverlay(boolean isMediaOverlay)226     public void setZOrderMediaOverlay(boolean isMediaOverlay) {
227         if (isMediaOverlay) {
228             mWindowZOrder = ZORDER_MEDIA_OVERLAY;
229             removeSessionOverlayView();
230         } else {
231             mWindowZOrder = ZORDER_MEDIA;
232             createSessionOverlayView();
233         }
234         if (mSurfaceView != null) {
235             // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
236             // from WindowLayoutParam as well as changes window type.
237             mSurfaceView.setZOrderOnTop(false);
238             mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
239         }
240     }
241 
242     /**
243      * Controls whether the TvView's surface is placed on top of its window. Normally it is placed
244      * behind the window, to allow it to (for the most part) appear to composite with the views in
245      * the hierarchy.  By setting this, you cause it to be placed above the window. This means that
246      * none of the contents of the window this TvView is in will be visible on top of its surface.
247      *
248      * <p>Note that this must be set before the TvView's containing window is attached to the window
249      * manager.
250      *
251      * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
252      *
253      * @param onTop {@code true} to be on top of its window, {@code false} otherwise.
254      */
setZOrderOnTop(boolean onTop)255     public void setZOrderOnTop(boolean onTop) {
256         if (onTop) {
257             mWindowZOrder = ZORDER_ON_TOP;
258             removeSessionOverlayView();
259         } else {
260             mWindowZOrder = ZORDER_MEDIA;
261             createSessionOverlayView();
262         }
263         if (mSurfaceView != null) {
264             mSurfaceView.setZOrderMediaOverlay(false);
265             mSurfaceView.setZOrderOnTop(onTop);
266         }
267      }
268 
269     /**
270      * Sets the relative stream volume of this TvView.
271      *
272      * <p>This method is primarily used to handle audio focus changes or mute a specific TvView when
273      * multiple views are displayed. If the method has not yet been called, the TvView assumes the
274      * default value of {@code 1.0f}.
275      *
276      * @param volume A volume value between {@code 0.0f} to {@code 1.0f}.
277      */
setStreamVolume(@loatRangefrom = 0.0, to = 1.0) float volume)278     public void setStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {
279         if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
280         mStreamVolume = volume;
281         if (mSession == null) {
282             // Volume will be set once the connection has been made.
283             return;
284         }
285         mSession.setStreamVolume(volume);
286     }
287 
288     /**
289      * Tunes to a given channel.
290      *
291      * @param inputId The ID of the TV input for the given channel.
292      * @param channelUri The URI of a channel.
293      */
tune(@onNull String inputId, Uri channelUri)294     public void tune(@NonNull String inputId, Uri channelUri) {
295         tune(inputId, channelUri, null);
296     }
297 
298     /**
299      * Tunes to a given channel. This can be used to provide domain-specific features that are only
300      * known between certain clients and their TV inputs.
301      *
302      * @param inputId The ID of TV input for the given channel.
303      * @param channelUri The URI of a channel.
304      * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
305      *            name, i.e. prefixed with a package name you own, so that different developers will
306      *            not create conflicting keys.
307      */
tune(String inputId, Uri channelUri, Bundle params)308     public void tune(String inputId, Uri channelUri, Bundle params) {
309         if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
310         if (TextUtils.isEmpty(inputId)) {
311             throw new IllegalArgumentException("inputId cannot be null or an empty string");
312         }
313         synchronized (sMainTvViewLock) {
314             if (sMainTvView.get() == null) {
315                 sMainTvView = new WeakReference<>(this);
316             }
317         }
318         if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) {
319             if (mSession != null) {
320                 mSession.tune(channelUri, params);
321             } else {
322                 // createSession() was called but the actual session for the given inputId has not
323                 // yet been created. Just replace the existing tuning params in the callback with
324                 // the new ones and tune later in onSessionCreated(). It is not necessary to create
325                 // a new callback because this tuning request was made on the same inputId.
326                 mSessionCallback.mChannelUri = channelUri;
327                 mSessionCallback.mTuneParams = params;
328             }
329         } else {
330             resetInternal();
331             // In case createSession() is called multiple times across different inputId's before
332             // any session is created (e.g. when quickly tuning to a channel from input A and then
333             // to another channel from input B), only the callback for the last createSession()
334             // should be invoked. (The previous callbacks are simply ignored.) To do that, we create
335             // a new callback each time and keep mSessionCallback pointing to the last one. If
336             // MySessionCallback.this is different from mSessionCallback, we know that this callback
337             // is obsolete and should ignore it.
338             mSessionCallback = new MySessionCallback(inputId, channelUri, params);
339             if (mTvInputManager != null) {
340                 mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
341             }
342         }
343     }
344 
345     /**
346      * Resets this TvView.
347      *
348      * <p>This method is primarily used to un-tune the current TvView.
349      */
reset()350     public void reset() {
351         if (DEBUG) Log.d(TAG, "reset()");
352         synchronized (sMainTvViewLock) {
353             if (this == sMainTvView.get()) {
354                 sMainTvView = NULL_TV_VIEW;
355             }
356         }
357         resetInternal();
358     }
359 
resetInternal()360     private void resetInternal() {
361         mSessionCallback = null;
362         mPendingAppPrivateCommands.clear();
363         if (mSession != null) {
364             setSessionSurface(null);
365             removeSessionOverlayView();
366             mUseRequestedSurfaceLayout = false;
367             mSession.release();
368             mSession = null;
369             resetSurfaceView();
370         }
371     }
372 
373     /**
374      * Requests to unblock TV content according to the given rating.
375      *
376      * <p>This notifies TV input that blocked content is now OK to play.
377      *
378      * @param unblockedRating A TvContentRating to unblock.
379      * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
380      * @removed
381      */
requestUnblockContent(TvContentRating unblockedRating)382     public void requestUnblockContent(TvContentRating unblockedRating) {
383         unblockContent(unblockedRating);
384     }
385 
386     /**
387      * Requests to unblock TV content according to the given rating.
388      *
389      * <p>This notifies TV input that blocked content is now OK to play.
390      *
391      * @param unblockedRating A TvContentRating to unblock.
392      * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
393      * @hide
394      */
395     @SystemApi
396     @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
unblockContent(TvContentRating unblockedRating)397     public void unblockContent(TvContentRating unblockedRating) {
398         if (mSession != null) {
399             mSession.unblockContent(unblockedRating);
400         }
401     }
402 
403     /**
404      * Enables or disables the caption in this TvView.
405      *
406      * <p>Note that this method does not take any effect unless the current TvView is tuned.
407      *
408      * @param enabled {@code true} to enable, {@code false} to disable.
409      */
setCaptionEnabled(boolean enabled)410     public void setCaptionEnabled(boolean enabled) {
411         if (DEBUG) Log.d(TAG, "setCaptionEnabled(" + enabled + ")");
412         mCaptionEnabled = enabled;
413         if (mSession != null) {
414             mSession.setCaptionEnabled(enabled);
415         }
416     }
417 
418     /**
419      * Selects a track.
420      *
421      * @param type The type of the track to select. The type can be {@link TvTrackInfo#TYPE_AUDIO},
422      *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
423      * @param trackId The ID of the track to select. {@code null} means to unselect the current
424      *            track for a given type.
425      * @see #getTracks
426      * @see #getSelectedTrack
427      */
selectTrack(int type, String trackId)428     public void selectTrack(int type, String trackId) {
429         if (mSession != null) {
430             mSession.selectTrack(type, trackId);
431         }
432     }
433 
434     /**
435      * Returns the list of tracks. Returns {@code null} if the information is not available.
436      *
437      * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
438      *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
439      * @see #selectTrack
440      * @see #getSelectedTrack
441      */
getTracks(int type)442     public List<TvTrackInfo> getTracks(int type) {
443         if (mSession == null) {
444             return null;
445         }
446         return mSession.getTracks(type);
447     }
448 
449     /**
450      * Returns the ID of the selected track for a given type. Returns {@code null} if the
451      * information is not available or the track is not selected.
452      *
453      * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
454      *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
455      * @see #selectTrack
456      * @see #getTracks
457      */
getSelectedTrack(int type)458     public String getSelectedTrack(int type) {
459         if (mSession == null) {
460             return null;
461         }
462         return mSession.getSelectedTrack(type);
463     }
464 
465     /**
466      * Plays a given recorded TV program.
467      *
468      * @param inputId The ID of the TV input that created the given recorded program.
469      * @param recordedProgramUri The URI of a recorded program.
470      */
timeShiftPlay(String inputId, Uri recordedProgramUri)471     public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
472         if (DEBUG) Log.d(TAG, "timeShiftPlay(" + recordedProgramUri + ")");
473         if (TextUtils.isEmpty(inputId)) {
474             throw new IllegalArgumentException("inputId cannot be null or an empty string");
475         }
476         synchronized (sMainTvViewLock) {
477             if (sMainTvView.get() == null) {
478                 sMainTvView = new WeakReference<>(this);
479             }
480         }
481         if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) {
482             if (mSession != null) {
483                 mSession.timeShiftPlay(recordedProgramUri);
484             } else {
485                 mSessionCallback.mRecordedProgramUri = recordedProgramUri;
486             }
487         } else {
488             resetInternal();
489             mSessionCallback = new MySessionCallback(inputId, recordedProgramUri);
490             if (mTvInputManager != null) {
491                 mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
492             }
493         }
494     }
495 
496     /**
497      * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume.
498      */
timeShiftPause()499     public void timeShiftPause() {
500         if (mSession != null) {
501             mSession.timeShiftPause();
502         }
503     }
504 
505     /**
506      * Resumes playback. No-op if it is already resumed. Call {@link #timeShiftPause} to pause.
507      */
timeShiftResume()508     public void timeShiftResume() {
509         if (mSession != null) {
510             mSession.timeShiftResume();
511         }
512     }
513 
514     /**
515      * Seeks to a specified time position. {@code timeMs} must be equal to or greater than the start
516      * position returned by {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged} and
517      * equal to or less than the current time.
518      *
519      * @param timeMs The time position to seek to, in milliseconds since the epoch.
520      */
timeShiftSeekTo(long timeMs)521     public void timeShiftSeekTo(long timeMs) {
522         if (mSession != null) {
523             mSession.timeShiftSeekTo(timeMs);
524         }
525     }
526 
527     /**
528      * Sets playback rate using {@link android.media.PlaybackParams}.
529      *
530      * @param params The playback params.
531      */
timeShiftSetPlaybackParams(@onNull PlaybackParams params)532     public void timeShiftSetPlaybackParams(@NonNull PlaybackParams params) {
533         if (mSession != null) {
534             mSession.timeShiftSetPlaybackParams(params);
535         }
536     }
537 
538     /**
539      * Sets the callback to be invoked when the time shift position is changed.
540      *
541      * @param callback The callback to receive time shift position changes. A value of {@code null}
542      *            removes the existing callback.
543      */
setTimeShiftPositionCallback(@ullable TimeShiftPositionCallback callback)544     public void setTimeShiftPositionCallback(@Nullable TimeShiftPositionCallback callback) {
545         mTimeShiftPositionCallback = callback;
546         ensurePositionTracking();
547     }
548 
ensurePositionTracking()549     private void ensurePositionTracking() {
550         if (mSession == null) {
551             return;
552         }
553         mSession.timeShiftEnablePositionTracking(mTimeShiftPositionCallback != null);
554     }
555 
556     /**
557      * Sends a private command to the underlying TV input. This can be used to provide
558      * domain-specific features that are only known between certain clients and their TV inputs.
559      *
560      * @param action The name of the private command to send. This <em>must</em> be a scoped name,
561      *            i.e. prefixed with a package name you own, so that different developers will not
562      *            create conflicting commands.
563      * @param data An optional bundle to send with the command.
564      */
sendAppPrivateCommand(@onNull String action, Bundle data)565     public void sendAppPrivateCommand(@NonNull String action, Bundle data) {
566         if (TextUtils.isEmpty(action)) {
567             throw new IllegalArgumentException("action cannot be null or an empty string");
568         }
569         if (mSession != null) {
570             mSession.sendAppPrivateCommand(action, data);
571         } else {
572             Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action
573                     + "\" pending)");
574             mPendingAppPrivateCommands.add(Pair.create(action, data));
575         }
576     }
577 
578     /**
579      * Dispatches an unhandled input event to the next receiver.
580      *
581      * <p>Except system keys, TvView always consumes input events in the normal flow. This is called
582      * asynchronously from where the event is dispatched. It gives the host application a chance to
583      * dispatch the unhandled input events.
584      *
585      * @param event The input event.
586      * @return {@code true} if the event was handled by the view, {@code false} otherwise.
587      */
dispatchUnhandledInputEvent(InputEvent event)588     public boolean dispatchUnhandledInputEvent(InputEvent event) {
589         if (mOnUnhandledInputEventListener != null) {
590             if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
591                 return true;
592             }
593         }
594         return onUnhandledInputEvent(event);
595     }
596 
597     /**
598      * Called when an unhandled input event also has not been handled by the user provided
599      * callback. This is the last chance to handle the unhandled input event in the TvView.
600      *
601      * @param event The input event.
602      * @return If you handled the event, return {@code true}. If you want to allow the event to be
603      *         handled by the next receiver, return {@code false}.
604      */
onUnhandledInputEvent(InputEvent event)605     public boolean onUnhandledInputEvent(InputEvent event) {
606         return false;
607     }
608 
609     /**
610      * Registers a callback to be invoked when an input event is not handled by the bound TV input.
611      *
612      * @param listener The callback to be invoked when the unhandled input event is received.
613      */
setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener)614     public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
615         mOnUnhandledInputEventListener = listener;
616     }
617 
618     @Override
dispatchKeyEvent(KeyEvent event)619     public boolean dispatchKeyEvent(KeyEvent event) {
620         if (super.dispatchKeyEvent(event)) {
621             return true;
622         }
623         if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
624         if (mSession == null) {
625             return false;
626         }
627         InputEvent copiedEvent = event.copy();
628         int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
629                 mHandler);
630         return ret != Session.DISPATCH_NOT_HANDLED;
631     }
632 
633     @Override
dispatchTouchEvent(MotionEvent event)634     public boolean dispatchTouchEvent(MotionEvent event) {
635         if (super.dispatchTouchEvent(event)) {
636             return true;
637         }
638         if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
639         if (mSession == null) {
640             return false;
641         }
642         InputEvent copiedEvent = event.copy();
643         int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
644                 mHandler);
645         return ret != Session.DISPATCH_NOT_HANDLED;
646     }
647 
648     @Override
dispatchTrackballEvent(MotionEvent event)649     public boolean dispatchTrackballEvent(MotionEvent event) {
650         if (super.dispatchTrackballEvent(event)) {
651             return true;
652         }
653         if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
654         if (mSession == null) {
655             return false;
656         }
657         InputEvent copiedEvent = event.copy();
658         int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
659                 mHandler);
660         return ret != Session.DISPATCH_NOT_HANDLED;
661     }
662 
663     @Override
dispatchGenericMotionEvent(MotionEvent event)664     public boolean dispatchGenericMotionEvent(MotionEvent event) {
665         if (super.dispatchGenericMotionEvent(event)) {
666             return true;
667         }
668         if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
669         if (mSession == null) {
670             return false;
671         }
672         InputEvent copiedEvent = event.copy();
673         int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
674                 mHandler);
675         return ret != Session.DISPATCH_NOT_HANDLED;
676     }
677 
678     @Override
dispatchWindowFocusChanged(boolean hasFocus)679     public void dispatchWindowFocusChanged(boolean hasFocus) {
680         super.dispatchWindowFocusChanged(hasFocus);
681         // Other app may have shown its own main TvView.
682         // Set main again to regain main session.
683         synchronized (sMainTvViewLock) {
684             if (hasFocus && this == sMainTvView.get() && mSession != null
685                     && checkChangeHdmiCecActiveSourcePermission()) {
686                 mSession.setMain();
687             }
688         }
689     }
690 
691     @Override
onAttachedToWindow()692     protected void onAttachedToWindow() {
693         super.onAttachedToWindow();
694         createSessionOverlayView();
695     }
696 
697     @Override
onDetachedFromWindow()698     protected void onDetachedFromWindow() {
699         removeSessionOverlayView();
700         super.onDetachedFromWindow();
701     }
702 
703     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)704     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
705         if (DEBUG) {
706             Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
707                     + ", bottom=" + bottom + ",)");
708         }
709         if (mUseRequestedSurfaceLayout) {
710             mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
711                     mSurfaceViewBottom);
712         } else {
713             mSurfaceView.layout(0, 0, right - left, bottom - top);
714         }
715     }
716 
717     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)718     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
719         mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
720         int width = mSurfaceView.getMeasuredWidth();
721         int height = mSurfaceView.getMeasuredHeight();
722         int childState = mSurfaceView.getMeasuredState();
723         setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
724                 resolveSizeAndState(height, heightMeasureSpec,
725                         childState << MEASURED_HEIGHT_STATE_SHIFT));
726     }
727 
728     @Override
gatherTransparentRegion(Region region)729     public boolean gatherTransparentRegion(Region region) {
730         if (mWindowZOrder != ZORDER_ON_TOP) {
731             if (region != null) {
732                 int width = getWidth();
733                 int height = getHeight();
734                 if (width > 0 && height > 0) {
735                     int location[] = new int[2];
736                     getLocationInWindow(location);
737                     int left = location[0];
738                     int top = location[1];
739                     region.op(left, top, left + width, top + height, Region.Op.UNION);
740                 }
741             }
742         }
743         return super.gatherTransparentRegion(region);
744     }
745 
746     @Override
draw(Canvas canvas)747     public void draw(Canvas canvas) {
748         if (mWindowZOrder != ZORDER_ON_TOP) {
749             // Punch a hole so that the underlying overlay view and surface can be shown.
750             canvas.drawColor(0, PorterDuff.Mode.CLEAR);
751         }
752         super.draw(canvas);
753     }
754 
755     @Override
dispatchDraw(Canvas canvas)756     protected void dispatchDraw(Canvas canvas) {
757         if (mWindowZOrder != ZORDER_ON_TOP) {
758             // Punch a hole so that the underlying overlay view and surface can be shown.
759             canvas.drawColor(0, PorterDuff.Mode.CLEAR);
760         }
761         super.dispatchDraw(canvas);
762     }
763 
764     @Override
onVisibilityChanged(View changedView, int visibility)765     protected void onVisibilityChanged(View changedView, int visibility) {
766         super.onVisibilityChanged(changedView, visibility);
767         mSurfaceView.setVisibility(visibility);
768         if (visibility == View.VISIBLE) {
769             createSessionOverlayView();
770         } else {
771             removeSessionOverlayView();
772         }
773     }
774 
resetSurfaceView()775     private void resetSurfaceView() {
776         if (mSurfaceView != null) {
777             mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
778             removeView(mSurfaceView);
779         }
780         mSurface = null;
781         mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
782             @Override
783             protected void updateSurface() {
784                 super.updateSurface();
785                 relayoutSessionOverlayView();
786             }};
787         // The surface view's content should be treated as secure all the time.
788         mSurfaceView.setSecure(true);
789         mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
790         if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) {
791             mSurfaceView.setZOrderMediaOverlay(true);
792         } else if (mWindowZOrder == ZORDER_ON_TOP) {
793             mSurfaceView.setZOrderOnTop(true);
794         }
795         addView(mSurfaceView);
796     }
797 
setSessionSurface(Surface surface)798     private void setSessionSurface(Surface surface) {
799         if (mSession == null) {
800             return;
801         }
802         mSession.setSurface(surface);
803     }
804 
dispatchSurfaceChanged(int format, int width, int height)805     private void dispatchSurfaceChanged(int format, int width, int height) {
806         if (mSession == null) {
807             return;
808         }
809         mSession.dispatchSurfaceChanged(format, width, height);
810     }
811 
createSessionOverlayView()812     private void createSessionOverlayView() {
813         if (mSession == null || !isAttachedToWindow()
814                 || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) {
815             return;
816         }
817         mOverlayViewFrame = getViewFrameOnScreen();
818         mSession.createOverlayView(this, mOverlayViewFrame);
819         mOverlayViewCreated = true;
820     }
821 
removeSessionOverlayView()822     private void removeSessionOverlayView() {
823         if (mSession == null || !mOverlayViewCreated) {
824             return;
825         }
826         mSession.removeOverlayView();
827         mOverlayViewCreated = false;
828         mOverlayViewFrame = null;
829     }
830 
relayoutSessionOverlayView()831     private void relayoutSessionOverlayView() {
832         if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated
833                 || mWindowZOrder != ZORDER_MEDIA) {
834             return;
835         }
836         Rect viewFrame = getViewFrameOnScreen();
837         if (viewFrame.equals(mOverlayViewFrame)) {
838             return;
839         }
840         mSession.relayoutOverlayView(viewFrame);
841         mOverlayViewFrame = viewFrame;
842     }
843 
getViewFrameOnScreen()844     private Rect getViewFrameOnScreen() {
845         Rect frame = new Rect();
846         getGlobalVisibleRect(frame);
847         RectF frameF = new RectF(frame);
848         getMatrix().mapRect(frameF);
849         frameF.round(frame);
850         return frame;
851     }
852 
checkChangeHdmiCecActiveSourcePermission()853     private boolean checkChangeHdmiCecActiveSourcePermission() {
854         return getContext().checkSelfPermission(
855                 android.Manifest.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE)
856                         == PackageManager.PERMISSION_GRANTED;
857     }
858 
859     /**
860      * Callback used to receive time shift position changes.
861      */
862     public abstract static class TimeShiftPositionCallback {
863 
864         /**
865          * This is called when the start position for time shifting has changed.
866          *
867          * <p>The start position for time shifting indicates the earliest possible time the user can
868          * seek to. Initially this is equivalent to the time when the underlying TV input starts
869          * recording. Later it may be adjusted because there is insufficient space or the duration
870          * of recording is limited. The application must not allow the user to seek to a position
871          * earlier than the start position.
872          *
873          * <p>For playback of a recorded program initiated by {@link #timeShiftPlay(String, Uri)},
874          * the start position is the time when playback starts. It does not change.
875          *
876          * @param inputId The ID of the TV input bound to this view.
877          * @param timeMs The start position for time shifting, in milliseconds since the epoch.
878          */
onTimeShiftStartPositionChanged(String inputId, long timeMs)879         public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
880         }
881 
882         /**
883          * This is called when the current position for time shifting has changed.
884          *
885          * <p>The current position for time shifting is the same as the current position of
886          * playback. During playback, the current position changes continuously. When paused, it
887          * does not change.
888          *
889          * <p>Note that {@code timeMs} is wall-clock time.
890          *
891          * @param inputId The ID of the TV input bound to this view.
892          * @param timeMs The current position for time shifting, in milliseconds since the epoch.
893          */
onTimeShiftCurrentPositionChanged(String inputId, long timeMs)894         public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
895         }
896     }
897 
898     /**
899      * Callback used to receive various status updates on the {@link TvView}.
900      */
901     public abstract static class TvInputCallback {
902 
903         /**
904          * This is invoked when an error occurred while establishing a connection to the underlying
905          * TV input.
906          *
907          * @param inputId The ID of the TV input bound to this view.
908          */
onConnectionFailed(String inputId)909         public void onConnectionFailed(String inputId) {
910         }
911 
912         /**
913          * This is invoked when the existing connection to the underlying TV input is lost.
914          *
915          * @param inputId The ID of the TV input bound to this view.
916          */
onDisconnected(String inputId)917         public void onDisconnected(String inputId) {
918         }
919 
920         /**
921          * This is invoked when the channel of this TvView is changed by the underlying TV input
922          * without any {@link TvView#tune} request.
923          *
924          * @param inputId The ID of the TV input bound to this view.
925          * @param channelUri The URI of a channel.
926          */
onChannelRetuned(String inputId, Uri channelUri)927         public void onChannelRetuned(String inputId, Uri channelUri) {
928         }
929 
930         /**
931          * This is called when the track information has been changed.
932          *
933          * @param inputId The ID of the TV input bound to this view.
934          * @param tracks A list which includes track information.
935          */
onTracksChanged(String inputId, List<TvTrackInfo> tracks)936         public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
937         }
938 
939         /**
940          * This is called when there is a change on the selected tracks.
941          *
942          * @param inputId The ID of the TV input bound to this view.
943          * @param type The type of the track selected. The type can be
944          *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
945          *            {@link TvTrackInfo#TYPE_SUBTITLE}.
946          * @param trackId The ID of the track selected.
947          */
onTrackSelected(String inputId, int type, String trackId)948         public void onTrackSelected(String inputId, int type, String trackId) {
949         }
950 
951         /**
952          * This is invoked when the video size has been changed. It is also called when the first
953          * time video size information becomes available after this view is tuned to a specific
954          * channel.
955          *
956          * @param inputId The ID of the TV input bound to this view.
957          * @param width The width of the video.
958          * @param height The height of the video.
959          */
onVideoSizeChanged(String inputId, int width, int height)960         public void onVideoSizeChanged(String inputId, int width, int height) {
961         }
962 
963         /**
964          * This is called when the video is available, so the TV input starts the playback.
965          *
966          * @param inputId The ID of the TV input bound to this view.
967          */
onVideoAvailable(String inputId)968         public void onVideoAvailable(String inputId) {
969         }
970 
971         /**
972          * This is called when the video is not available, so the TV input stops the playback.
973          *
974          * @param inputId The ID of the TV input bound to this view.
975          * @param reason The reason why the TV input stopped the playback:
976          * <ul>
977          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
978          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
979          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
980          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
981          * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}
982          * </ul>
983          */
onVideoUnavailable( String inputId, @TvInputManager.VideoUnavailableReason int reason)984         public void onVideoUnavailable(
985                 String inputId, @TvInputManager.VideoUnavailableReason int reason) {
986         }
987 
988         /**
989          * This is called when the current program content turns out to be allowed to watch since
990          * its content rating is not blocked by parental controls.
991          *
992          * @param inputId The ID of the TV input bound to this view.
993          */
onContentAllowed(String inputId)994         public void onContentAllowed(String inputId) {
995         }
996 
997         /**
998          * This is called when the current program content turns out to be not allowed to watch
999          * since its content rating is blocked by parental controls.
1000          *
1001          * @param inputId The ID of the TV input bound to this view.
1002          * @param rating The content rating of the blocked program.
1003          */
onContentBlocked(String inputId, TvContentRating rating)1004         public void onContentBlocked(String inputId, TvContentRating rating) {
1005         }
1006 
1007         /**
1008          * This is invoked when a custom event from the bound TV input is sent to this view.
1009          *
1010          * @param inputId The ID of the TV input bound to this view.
1011          * @param eventType The type of the event.
1012          * @param eventArgs Optional arguments of the event.
1013          * @hide
1014          */
1015         @SystemApi
onEvent(String inputId, String eventType, Bundle eventArgs)1016         public void onEvent(String inputId, String eventType, Bundle eventArgs) {
1017         }
1018 
1019         /**
1020          * This is called when the time shift status is changed.
1021          *
1022          * @param inputId The ID of the TV input bound to this view.
1023          * @param status The current time shift status. Should be one of the followings.
1024          * <ul>
1025          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
1026          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
1027          * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
1028          * </ul>
1029          */
onTimeShiftStatusChanged( String inputId, @TvInputManager.TimeShiftStatus int status)1030         public void onTimeShiftStatusChanged(
1031                 String inputId, @TvInputManager.TimeShiftStatus int status) {
1032         }
1033     }
1034 
1035     /**
1036      * Interface definition for a callback to be invoked when the unhandled input event is received.
1037      */
1038     public interface OnUnhandledInputEventListener {
1039         /**
1040          * Called when an input event was not handled by the bound TV input.
1041          *
1042          * <p>This is called asynchronously from where the event is dispatched. It gives the host
1043          * application a chance to handle the unhandled input events.
1044          *
1045          * @param event The input event.
1046          * @return If you handled the event, return {@code true}. If you want to allow the event to
1047          *         be handled by the next receiver, return {@code false}.
1048          */
onUnhandledInputEvent(InputEvent event)1049         boolean onUnhandledInputEvent(InputEvent event);
1050     }
1051 
1052     private class MySessionCallback extends SessionCallback {
1053         final String mInputId;
1054         Uri mChannelUri;
1055         Bundle mTuneParams;
1056         Uri mRecordedProgramUri;
1057 
MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams)1058         MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
1059             mInputId = inputId;
1060             mChannelUri = channelUri;
1061             mTuneParams = tuneParams;
1062         }
1063 
MySessionCallback(String inputId, Uri recordedProgramUri)1064         MySessionCallback(String inputId, Uri recordedProgramUri) {
1065             mInputId = inputId;
1066             mRecordedProgramUri = recordedProgramUri;
1067         }
1068 
1069         @Override
onSessionCreated(Session session)1070         public void onSessionCreated(Session session) {
1071             if (DEBUG) {
1072                 Log.d(TAG, "onSessionCreated()");
1073             }
1074             if (this != mSessionCallback) {
1075                 Log.w(TAG, "onSessionCreated - session already created");
1076                 // This callback is obsolete.
1077                 if (session != null) {
1078                     session.release();
1079                 }
1080                 return;
1081             }
1082             mSession = session;
1083             if (session != null) {
1084                 // Sends the pending app private commands first.
1085                 for (Pair<String, Bundle> command : mPendingAppPrivateCommands) {
1086                     mSession.sendAppPrivateCommand(command.first, command.second);
1087                 }
1088                 mPendingAppPrivateCommands.clear();
1089 
1090                 synchronized (sMainTvViewLock) {
1091                     if (hasWindowFocus() && TvView.this == sMainTvView.get()
1092                             && checkChangeHdmiCecActiveSourcePermission()) {
1093                         mSession.setMain();
1094                     }
1095                 }
1096                 // mSurface may not be ready yet as soon as starting an application.
1097                 // In the case, we don't send Session.setSurface(null) unnecessarily.
1098                 // setSessionSurface will be called in surfaceCreated.
1099                 if (mSurface != null) {
1100                     setSessionSurface(mSurface);
1101                     if (mSurfaceChanged) {
1102                         dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
1103                     }
1104                 }
1105                 createSessionOverlayView();
1106                 if (mStreamVolume != null) {
1107                     mSession.setStreamVolume(mStreamVolume);
1108                 }
1109                 if (mCaptionEnabled != null) {
1110                     mSession.setCaptionEnabled(mCaptionEnabled);
1111                 }
1112                 if (mChannelUri != null) {
1113                     mSession.tune(mChannelUri, mTuneParams);
1114                 } else {
1115                     mSession.timeShiftPlay(mRecordedProgramUri);
1116                 }
1117                 ensurePositionTracking();
1118             } else {
1119                 mSessionCallback = null;
1120                 if (mCallback != null) {
1121                     mCallback.onConnectionFailed(mInputId);
1122                 }
1123             }
1124         }
1125 
1126         @Override
onSessionReleased(Session session)1127         public void onSessionReleased(Session session) {
1128             if (DEBUG) {
1129                 Log.d(TAG, "onSessionReleased()");
1130             }
1131             if (this != mSessionCallback) {
1132                 Log.w(TAG, "onSessionReleased - session not created");
1133                 return;
1134             }
1135             mOverlayViewCreated = false;
1136             mOverlayViewFrame = null;
1137             mSessionCallback = null;
1138             mSession = null;
1139             if (mCallback != null) {
1140                 mCallback.onDisconnected(mInputId);
1141             }
1142         }
1143 
1144         @Override
onChannelRetuned(Session session, Uri channelUri)1145         public void onChannelRetuned(Session session, Uri channelUri) {
1146             if (DEBUG) {
1147                 Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
1148             }
1149             if (this != mSessionCallback) {
1150                 Log.w(TAG, "onChannelRetuned - session not created");
1151                 return;
1152             }
1153             if (mCallback != null) {
1154                 mCallback.onChannelRetuned(mInputId, channelUri);
1155             }
1156         }
1157 
1158         @Override
onTracksChanged(Session session, List<TvTrackInfo> tracks)1159         public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
1160             if (DEBUG) {
1161                 Log.d(TAG, "onTracksChanged(" + tracks + ")");
1162             }
1163             if (this != mSessionCallback) {
1164                 Log.w(TAG, "onTracksChanged - session not created");
1165                 return;
1166             }
1167             if (mCallback != null) {
1168                 mCallback.onTracksChanged(mInputId, tracks);
1169             }
1170         }
1171 
1172         @Override
onTrackSelected(Session session, int type, String trackId)1173         public void onTrackSelected(Session session, int type, String trackId) {
1174             if (DEBUG) {
1175                 Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
1176             }
1177             if (this != mSessionCallback) {
1178                 Log.w(TAG, "onTrackSelected - session not created");
1179                 return;
1180             }
1181             if (mCallback != null) {
1182                 mCallback.onTrackSelected(mInputId, type, trackId);
1183             }
1184         }
1185 
1186         @Override
onVideoSizeChanged(Session session, int width, int height)1187         public void onVideoSizeChanged(Session session, int width, int height) {
1188             if (DEBUG) {
1189                 Log.d(TAG, "onVideoSizeChanged()");
1190             }
1191             if (this != mSessionCallback) {
1192                 Log.w(TAG, "onVideoSizeChanged - session not created");
1193                 return;
1194             }
1195             if (mCallback != null) {
1196                 mCallback.onVideoSizeChanged(mInputId, width, height);
1197             }
1198         }
1199 
1200         @Override
onVideoAvailable(Session session)1201         public void onVideoAvailable(Session session) {
1202             if (DEBUG) {
1203                 Log.d(TAG, "onVideoAvailable()");
1204             }
1205             if (this != mSessionCallback) {
1206                 Log.w(TAG, "onVideoAvailable - session not created");
1207                 return;
1208             }
1209             if (mCallback != null) {
1210                 mCallback.onVideoAvailable(mInputId);
1211             }
1212         }
1213 
1214         @Override
onVideoUnavailable(Session session, int reason)1215         public void onVideoUnavailable(Session session, int reason) {
1216             if (DEBUG) {
1217                 Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")");
1218             }
1219             if (this != mSessionCallback) {
1220                 Log.w(TAG, "onVideoUnavailable - session not created");
1221                 return;
1222             }
1223             if (mCallback != null) {
1224                 mCallback.onVideoUnavailable(mInputId, reason);
1225             }
1226         }
1227 
1228         @Override
onContentAllowed(Session session)1229         public void onContentAllowed(Session session) {
1230             if (DEBUG) {
1231                 Log.d(TAG, "onContentAllowed()");
1232             }
1233             if (this != mSessionCallback) {
1234                 Log.w(TAG, "onContentAllowed - session not created");
1235                 return;
1236             }
1237             if (mCallback != null) {
1238                 mCallback.onContentAllowed(mInputId);
1239             }
1240         }
1241 
1242         @Override
onContentBlocked(Session session, TvContentRating rating)1243         public void onContentBlocked(Session session, TvContentRating rating) {
1244             if (DEBUG) {
1245                 Log.d(TAG, "onContentBlocked(rating=" + rating + ")");
1246             }
1247             if (this != mSessionCallback) {
1248                 Log.w(TAG, "onContentBlocked - session not created");
1249                 return;
1250             }
1251             if (mCallback != null) {
1252                 mCallback.onContentBlocked(mInputId, rating);
1253             }
1254         }
1255 
1256         @Override
onLayoutSurface(Session session, int left, int top, int right, int bottom)1257         public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
1258             if (DEBUG) {
1259                 Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
1260                         + right + ", bottom=" + bottom + ",)");
1261             }
1262             if (this != mSessionCallback) {
1263                 Log.w(TAG, "onLayoutSurface - session not created");
1264                 return;
1265             }
1266             mSurfaceViewLeft = left;
1267             mSurfaceViewTop = top;
1268             mSurfaceViewRight = right;
1269             mSurfaceViewBottom = bottom;
1270             mUseRequestedSurfaceLayout = true;
1271             requestLayout();
1272         }
1273 
1274         @Override
onSessionEvent(Session session, String eventType, Bundle eventArgs)1275         public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
1276             if (DEBUG) {
1277                 Log.d(TAG, "onSessionEvent(" + eventType + ")");
1278             }
1279             if (this != mSessionCallback) {
1280                 Log.w(TAG, "onSessionEvent - session not created");
1281                 return;
1282             }
1283             if (mCallback != null) {
1284                 mCallback.onEvent(mInputId, eventType, eventArgs);
1285             }
1286         }
1287 
1288         @Override
onTimeShiftStatusChanged(Session session, int status)1289         public void onTimeShiftStatusChanged(Session session, int status) {
1290             if (DEBUG) {
1291                 Log.d(TAG, "onTimeShiftStatusChanged()");
1292             }
1293             if (this != mSessionCallback) {
1294                 Log.w(TAG, "onTimeShiftStatusChanged - session not created");
1295                 return;
1296             }
1297             if (mCallback != null) {
1298                 mCallback.onTimeShiftStatusChanged(mInputId, status);
1299             }
1300         }
1301 
1302         @Override
onTimeShiftStartPositionChanged(Session session, long timeMs)1303         public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
1304             if (DEBUG) {
1305                 Log.d(TAG, "onTimeShiftStartPositionChanged()");
1306             }
1307             if (this != mSessionCallback) {
1308                 Log.w(TAG, "onTimeShiftStartPositionChanged - session not created");
1309                 return;
1310             }
1311             if (mTimeShiftPositionCallback != null) {
1312                 mTimeShiftPositionCallback.onTimeShiftStartPositionChanged(mInputId, timeMs);
1313             }
1314         }
1315 
1316         @Override
onTimeShiftCurrentPositionChanged(Session session, long timeMs)1317         public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
1318             if (DEBUG) {
1319                 Log.d(TAG, "onTimeShiftCurrentPositionChanged()");
1320             }
1321             if (this != mSessionCallback) {
1322                 Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created");
1323                 return;
1324             }
1325             if (mTimeShiftPositionCallback != null) {
1326                 mTimeShiftPositionCallback.onTimeShiftCurrentPositionChanged(mInputId, timeMs);
1327             }
1328         }
1329     }
1330 }
1331