1 /*
2  * Copyright (C) 2022 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 com.android.tv.samples.sampletvinteractiveappservice;
18 
19 import android.annotation.TargetApi;
20 import android.app.Presentation;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.graphics.PixelFormat;
24 import android.graphics.Rect;
25 import android.graphics.drawable.ColorDrawable;
26 import android.hardware.display.DisplayManager;
27 import android.hardware.display.VirtualDisplay;
28 import android.media.MediaPlayer;
29 import android.media.tv.AdRequest;
30 import android.media.tv.AdResponse;
31 import android.media.tv.BroadcastInfoRequest;
32 import android.media.tv.BroadcastInfoResponse;
33 import android.media.tv.SectionRequest;
34 import android.media.tv.SectionResponse;
35 import android.media.tv.StreamEventRequest;
36 import android.media.tv.StreamEventResponse;
37 import android.media.tv.TableRequest;
38 import android.media.tv.TableResponse;
39 import android.media.tv.TvTrackInfo;
40 import android.media.tv.interactive.AppLinkInfo;
41 import android.media.tv.interactive.TvInteractiveAppManager;
42 import android.media.tv.interactive.TvInteractiveAppService;
43 import android.net.Uri;
44 import android.os.Build;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.ParcelFileDescriptor;
48 import android.text.TextUtils;
49 import android.util.DisplayMetrics;
50 import android.util.Log;
51 import android.view.KeyEvent;
52 import android.view.LayoutInflater;
53 import android.view.Surface;
54 import android.view.SurfaceHolder;
55 import android.view.SurfaceView;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.view.WindowManager;
59 import android.widget.FrameLayout;
60 import android.widget.LinearLayout;
61 import android.widget.TextView;
62 import android.widget.VideoView;
63 
64 import androidx.annotation.NonNull;
65 
66 import java.io.RandomAccessFile;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.List;
70 
71 public class TiasSessionImpl extends TvInteractiveAppService.Session {
72     private static final String TAG = "SampleTvInteractiveAppService";
73     private static final boolean DEBUG = true;
74 
75     private static final String VIRTUAL_DISPLAY_NAME = "sample_tias_display";
76 
77     // For testing purposes, limit the number of response for a single request
78     private static final int MAX_HANDLED_RESPONSE = 3;
79 
80     private final Context mContext;
81     private TvInteractiveAppManager mTvIAppManager;
82     private final Handler mHandler;
83     private final String mAppServiceId;
84     private final int mType;
85     private final ViewGroup mViewContainer;
86     private Surface mSurface;
87     private VirtualDisplay mVirtualDisplay;
88     private List<TvTrackInfo> mTracks;
89 
90     private TextView mTvInputIdView;
91     private TextView mChannelUriView;
92     private TextView mVideoTrackView;
93     private TextView mAudioTrackView;
94     private TextView mSubtitleTrackView;
95     private TextView mLogView;
96 
97     private VideoView mVideoView;
98     private SurfaceView mAdSurfaceView;
99     private Surface mAdSurface;
100     private ParcelFileDescriptor mAdFd;
101     private FrameLayout mMediaContainer;
102     private int mAdState;
103     private int mWidth;
104     private int mHeight;
105     private int mScreenWidth;
106     private int mScreenHeight;
107     private String mCurrentTvInputId;
108     private Uri mCurrentChannelUri;
109     private String mSelectingAudioTrackId;
110     private String mFirstAudioTrackId;
111     private int mGeneratedRequestId = 0;
112     private boolean mRequestStreamEventFinished = false;
113     private int mSectionReceived = 0;
114     private List<String> mStreamDataList = new ArrayList<>();
115     private boolean mIsFullScreen = true;
116 
TiasSessionImpl(Context context, String iAppServiceId, int type)117     public TiasSessionImpl(Context context, String iAppServiceId, int type) {
118         super(context);
119         if (DEBUG) {
120             Log.d(TAG, "Constructing service with iAppServiceId=" + iAppServiceId
121                     + " type=" + type);
122         }
123         mContext = context;
124         mAppServiceId = iAppServiceId;
125         mType = type;
126         mHandler = new Handler(context.getMainLooper());
127         mTvIAppManager = (TvInteractiveAppManager) mContext.getSystemService(
128                 Context.TV_INTERACTIVE_APP_SERVICE);
129 
130         mViewContainer = new LinearLayout(context);
131         mViewContainer.setBackground(new ColorDrawable(0));
132     }
133 
134     @Override
onCreateMediaView()135     public View onCreateMediaView() {
136         mAdSurfaceView = new SurfaceView(mContext);
137         if (DEBUG) {
138             Log.d(TAG, "create surfaceView");
139         }
140         mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
141         mAdSurfaceView
142                 .getHolder()
143                 .addCallback(
144                         new SurfaceHolder.Callback() {
145                             @Override
146                             public void surfaceCreated(SurfaceHolder holder) {
147                                 mAdSurface = holder.getSurface();
148                             }
149 
150                             @Override
151                             public void surfaceChanged(
152                                     SurfaceHolder holder, int format, int width, int height) {
153                                 mAdSurface = holder.getSurface();
154                             }
155 
156                             @Override
157                             public void surfaceDestroyed(SurfaceHolder holder) {}
158                         });
159         mAdSurfaceView.setVisibility(View.INVISIBLE);
160         ViewGroup.LayoutParams layoutParams =
161                 new ViewGroup.LayoutParams(
162                         ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
163         mAdSurfaceView.setLayoutParams(layoutParams);
164         mMediaContainer.addView(mVideoView);
165         mMediaContainer.addView(mAdSurfaceView);
166         return mMediaContainer;
167     }
168 
169     @Override
onAdResponse(AdResponse adResponse)170     public void onAdResponse(AdResponse adResponse) {
171         mAdState = adResponse.getResponseType();
172         switch (mAdState) {
173             case AdResponse.RESPONSE_TYPE_PLAYING:
174                 long time = adResponse.getElapsedTimeMillis();
175                 updateLogText("AD is playing. " + time);
176                 break;
177             case AdResponse.RESPONSE_TYPE_STOPPED:
178                 updateLogText("AD is stopped.");
179                 mAdSurfaceView.setVisibility(View.INVISIBLE);
180                 break;
181             case AdResponse.RESPONSE_TYPE_FINISHED:
182                 updateLogText("AD is play finished.");
183                 mAdSurfaceView.setVisibility(View.INVISIBLE);
184                 break;
185         }
186     }
187 
188     @Override
onRelease()189     public void onRelease() {
190         if (DEBUG) {
191             Log.d(TAG, "onRelease");
192         }
193         if (mSurface != null) {
194             mSurface.release();
195             mSurface = null;
196         }
197         if (mVirtualDisplay != null) {
198             mVirtualDisplay.release();
199             mVirtualDisplay = null;
200         }
201     }
202 
203     @Override
onSetSurface(Surface surface)204     public boolean onSetSurface(Surface surface) {
205         if (DEBUG) {
206             Log.d(TAG, "onSetSurface");
207         }
208         if (mSurface != null) {
209             mSurface.release();
210         }
211         updateSurface(surface, mWidth, mHeight);
212         mSurface = surface;
213         return true;
214     }
215 
216     @Override
onSurfaceChanged(int format, int width, int height)217     public void onSurfaceChanged(int format, int width, int height) {
218         if (DEBUG) {
219             Log.d(TAG, "onSurfaceChanged format=" + format + " width=" + width +
220                     " height=" + height);
221         }
222         if (mSurface != null) {
223             updateSurface(mSurface, width, height);
224             mWidth = width;
225             mHeight = height;
226         }
227     }
228 
229     @Override
onStartInteractiveApp()230     public void onStartInteractiveApp() {
231         if (DEBUG) {
232             Log.d(TAG, "onStartInteractiveApp");
233         }
234         mHandler.post(
235                 () -> {
236                     initSampleView();
237                     setMediaViewEnabled(true);
238                     requestCurrentTvInputId();
239                     requestCurrentChannelUri();
240                     requestTrackInfoList();
241                 }
242         );
243     }
244 
245     @Override
onStopInteractiveApp()246     public void onStopInteractiveApp() {
247         if (DEBUG) {
248             Log.d(TAG, "onStopInteractiveApp");
249         }
250     }
251 
prepare(TvInteractiveAppService serviceCaller)252     public void prepare(TvInteractiveAppService serviceCaller) {
253         // Slightly delay our post to ensure the Manager has had time to register our Session
254         mHandler.postDelayed(
255                 () -> {
256                     if (serviceCaller != null) {
257                         serviceCaller.notifyStateChanged(mType,
258                                 TvInteractiveAppManager.SERVICE_STATE_READY,
259                                 TvInteractiveAppManager.ERROR_NONE);
260                     }
261                 },
262                 100);
263     }
264 
265     @Override
onKeyDown(int keyCode, @NonNull KeyEvent event)266     public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
267         // TODO: use a menu view instead of key events for the following tests
268         switch (keyCode) {
269             case KeyEvent.KEYCODE_PROG_RED:
270                 tuneToNextChannel();
271                 return true;
272             case KeyEvent.KEYCODE_A:
273                 updateLogText("stop video broadcast begin");
274                 tuneChannelByType(
275                         TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
276                         mCurrentTvInputId,
277                         null);
278                 updateLogText("stop video broadcast end");
279                 return true;
280             case KeyEvent.KEYCODE_B:
281                 updateLogText("resume video broadcast begin");
282                 tuneChannelByType(
283                         TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
284                         mCurrentTvInputId,
285                         mCurrentChannelUri);
286                 updateLogText("resume video broadcast end");
287                 return true;
288             case KeyEvent.KEYCODE_C:
289                 updateLogText("unselect audio track");
290                 mSelectingAudioTrackId = null;
291                 selectTrack(TvTrackInfo.TYPE_AUDIO, null);
292                 return true;
293             case KeyEvent.KEYCODE_D:
294                 updateLogText("select audio track " + mFirstAudioTrackId);
295                 mSelectingAudioTrackId = mFirstAudioTrackId;
296                 selectTrack(TvTrackInfo.TYPE_AUDIO, mFirstAudioTrackId);
297                 return true;
298             case KeyEvent.KEYCODE_E:
299                 if (mVideoView != null) {
300                     if (mVideoView.isPlaying()) {
301                         updateLogText("stop media");
302                         mVideoView.stopPlayback();
303                         mVideoView.setVisibility(View.GONE);
304                         tuneChannelByType(
305                                 TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
306                                 mCurrentTvInputId,
307                                 mCurrentChannelUri);
308                     } else {
309                         updateLogText("play media");
310                         tuneChannelByType(
311                                 TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
312                                 mCurrentTvInputId,
313                                 null);
314                         mVideoView.setVisibility(View.VISIBLE);
315                         // TODO: put a file sample.mp4 in res/raw/ and use R.raw.sample for the URI
316                         Uri uri = Uri.parse(
317                                 "android.resource://" + mContext.getPackageName() + "/");
318                         mVideoView.setVideoURI(uri);
319                         mVideoView.start();
320                         updateLogText("media is playing");
321                     }
322                 }
323                 return true;
324             case KeyEvent.KEYCODE_F:
325                 updateLogText("request StreamEvent");
326                 mRequestStreamEventFinished = false;
327                 mStreamDataList.clear();
328                 // TODO: build target URI instead of using channel URI
329                 requestStreamEvent(
330                         mCurrentChannelUri == null ? null : mCurrentChannelUri.toString(),
331                         "event1");
332                 return true;
333             case KeyEvent.KEYCODE_G:
334                 updateLogText("change video bounds");
335                 if (mIsFullScreen) {
336                     setVideoBounds(new Rect(100, 150, 960, 540));
337                     updateLogText("Change video broadcast size(100, 150, 960, 540)");
338                     mIsFullScreen = false;
339                 } else {
340                     setVideoBounds(new Rect(0, 0, mScreenWidth, mScreenHeight));
341                     updateLogText("Change video broadcast full screen");
342                     mIsFullScreen = true;
343                 }
344                 return true;
345             case KeyEvent.KEYCODE_H:
346                 updateLogText("request section");
347                 mSectionReceived = 0;
348                 requestSection(false, 0, 0x0, -1);
349                 return true;
350             case KeyEvent.KEYCODE_I:
351                 if (mTvIAppManager == null) {
352                     updateLogText("TvIAppManager null");
353                     return false;
354                 }
355                 List<AppLinkInfo> appLinks = getAppLinkInfoList();
356                 if (appLinks.isEmpty()) {
357                     updateLogText("Not found AppLink");
358                 } else {
359                     AppLinkInfo appLink = appLinks.get(0);
360                     Intent intent = new Intent();
361                     intent.setComponent(appLink.getComponentName());
362                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
363                     mContext.getApplicationContext().startActivity(intent);
364                     updateLogText("Launch " + appLink.getComponentName());
365                 }
366                 return true;
367             case KeyEvent.KEYCODE_J:
368                 updateLogText("Request SI Tables ");
369                 // Network Information Table (NIT)
370                 requestTable(false, 0x40, /* TableRequest.TABLE_NAME_NIT */ 3, -1);
371                 // Service Description Table (SDT)
372                 requestTable(false, 0x42, /* TableRequest.TABLE_NAME_SDT */ 5, -1);
373                 // Event Information Table (EIT)
374                 requestTable(false, 0x4e, /* TableRequest.TABLE_NAME_EIT */ 6, -1);
375                 return true;
376             case KeyEvent.KEYCODE_K:
377                 updateLogText("Request Video Bounds");
378                 requestCurrentVideoBoundsWrapper();
379                 return true;
380             case KeyEvent.KEYCODE_L: {
381                 updateLogText("stop video broadcast with blank mode");
382                 Bundle params = new Bundle();
383                 params.putInt(
384                         /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
385                         "command_stop_mode",
386                         /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK */
387                         1);
388                 tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
389                         mCurrentTvInputId, null, params);
390                 return true;
391             }
392             case KeyEvent.KEYCODE_M: {
393                 updateLogText("stop video broadcast with freeze mode");
394                 Bundle params = new Bundle();
395                 params.putInt(
396                         /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
397                         "command_stop_mode",
398                         /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_FREEZE */
399                         2);
400                 tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
401                         mCurrentTvInputId, null, params);
402                 return true;
403             }
404             case KeyEvent.KEYCODE_N: {
405                 updateLogText("request AD");
406                 requestAd();
407                 return true;
408             }
409             default:
410                 return super.onKeyDown(keyCode, event);
411         }
412     }
413 
414     @Override
onKeyUp(int keyCode, @NonNull KeyEvent event)415     public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
416         switch (keyCode) {
417             case KeyEvent.KEYCODE_PROG_RED:
418             case KeyEvent.KEYCODE_A:
419             case KeyEvent.KEYCODE_B:
420             case KeyEvent.KEYCODE_C:
421             case KeyEvent.KEYCODE_D:
422             case KeyEvent.KEYCODE_E:
423             case KeyEvent.KEYCODE_F:
424             case KeyEvent.KEYCODE_G:
425             case KeyEvent.KEYCODE_H:
426             case KeyEvent.KEYCODE_I:
427             case KeyEvent.KEYCODE_J:
428             case KeyEvent.KEYCODE_K:
429             case KeyEvent.KEYCODE_L:
430             case KeyEvent.KEYCODE_M:
431             case KeyEvent.KEYCODE_N:
432                 return true;
433             default:
434                 return super.onKeyUp(keyCode, event);
435         }
436     }
437 
updateLogText(String log)438     public void updateLogText(String log) {
439         if (DEBUG) {
440             Log.d(TAG, log);
441         }
442         mLogView.setText(log);
443     }
444 
updateSurface(Surface surface, int width, int height)445     private void updateSurface(Surface surface, int width, int height) {
446         mHandler.post(
447                 () -> {
448                     // Update our virtualDisplay if it already exists, create a new one otherwise
449                     if (mVirtualDisplay != null) {
450                         mVirtualDisplay.setSurface(surface);
451                         mVirtualDisplay.resize(width, height, DisplayMetrics.DENSITY_DEFAULT);
452                     } else {
453                         DisplayManager displayManager =
454                                 mContext.getSystemService(DisplayManager.class);
455                         if (displayManager == null) {
456                             Log.e(TAG, "Failed to get DisplayManager");
457                             return;
458                         }
459                         mVirtualDisplay = displayManager.createVirtualDisplay(VIRTUAL_DISPLAY_NAME,
460                                         width,
461                                         height,
462                                         DisplayMetrics.DENSITY_DEFAULT,
463                                         surface,
464                                         DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
465 
466                         Presentation presentation =
467                                 new Presentation(mContext, mVirtualDisplay.getDisplay());
468                         presentation.setContentView(mViewContainer);
469                         presentation.getWindow().setBackgroundDrawable(new ColorDrawable(0));
470                         presentation.show();
471                     }
472                 });
473     }
474 
initSampleView()475     private void initSampleView() {
476         View sampleView = LayoutInflater.from(mContext).inflate(R.layout.sample_layout, null);
477         TextView appServiceIdText = sampleView.findViewById(R.id.app_service_id);
478         appServiceIdText.setText("App Service ID: " + mAppServiceId);
479 
480         mTvInputIdView = sampleView.findViewById(R.id.tv_input_id);
481         mChannelUriView = sampleView.findViewById(R.id.channel_uri);
482         mVideoTrackView = sampleView.findViewById(R.id.video_track_selected);
483         mAudioTrackView = sampleView.findViewById(R.id.audio_track_selected);
484         mSubtitleTrackView = sampleView.findViewById(R.id.subtitle_track_selected);
485         mLogView = sampleView.findViewById(R.id.log_text);
486         // Set default values for the selected tracks, since we cannot request data on them directly
487         mVideoTrackView.setText("No video track selected");
488         mAudioTrackView.setText("No audio track selected");
489         mSubtitleTrackView.setText("No subtitle track selected");
490 
491         mVideoView = new VideoView(mContext);
492         mVideoView.setVisibility(View.GONE);
493         mVideoView.setOnCompletionListener(
494                 new MediaPlayer.OnCompletionListener() {
495                     @Override
496                     public void onCompletion(MediaPlayer mediaPlayer) {
497                         mVideoView.setVisibility(View.GONE);
498                         mLogView.setText("MediaPlayer onCompletion");
499                         tuneChannelByType(
500                                 TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE,
501                                 mCurrentTvInputId,
502                                 mCurrentChannelUri);
503                     }
504                 });
505         mWidth = 0;
506         mHeight = 0;
507         WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
508         mScreenWidth = wm.getDefaultDisplay().getWidth();
509         mScreenHeight = wm.getDefaultDisplay().getHeight();
510 
511         mViewContainer.addView(sampleView);
512     }
513 
updateTrackSelectedView(int type, String trackId)514     private void updateTrackSelectedView(int type, String trackId) {
515         mHandler.post(
516                 () -> {
517                     if (mTracks == null) {
518                         return;
519                     }
520                     TvTrackInfo newSelectedTrack = null;
521                     for (TvTrackInfo track : mTracks) {
522                         if (track.getType() == type && track.getId().equals(trackId)) {
523                             newSelectedTrack = track;
524                             break;
525                         }
526                     }
527 
528                     if (newSelectedTrack == null) {
529                         if (DEBUG) {
530                             Log.d(TAG, "Did not find selected track within track list");
531                         }
532                         return;
533                     }
534                     switch (newSelectedTrack.getType()) {
535                         case TvTrackInfo.TYPE_VIDEO:
536                             mVideoTrackView.setText(
537                                     "Video Track: id= " + newSelectedTrack.getId()
538                                     + ", height=" + newSelectedTrack.getVideoHeight()
539                                     + ", width=" + newSelectedTrack.getVideoWidth()
540                                     + ", frame_rate=" + newSelectedTrack.getVideoFrameRate()
541                                     + ", pixel_ratio=" + newSelectedTrack.getVideoPixelAspectRatio()
542                             );
543                             break;
544                         case TvTrackInfo.TYPE_AUDIO:
545                             mAudioTrackView.setText(
546                                     "Audio Track: id=" + newSelectedTrack.getId()
547                                     + ", lang=" + newSelectedTrack.getLanguage()
548                                     + ", sample_rate=" + newSelectedTrack.getAudioSampleRate()
549                                     + ", channel_count=" + newSelectedTrack.getAudioChannelCount()
550                             );
551                             break;
552                         case TvTrackInfo.TYPE_SUBTITLE:
553                             mSubtitleTrackView.setText(
554                                     "Subtitle Track: id=" + newSelectedTrack.getId()
555                                     + ", lang=" + newSelectedTrack.getLanguage()
556                             );
557                             break;
558                     }
559                 }
560         );
561     }
562 
tuneChannelByType(String type, String inputId, Uri channelUri, Bundle bundle)563     private void tuneChannelByType(String type, String inputId, Uri channelUri, Bundle bundle) {
564         Bundle parameters = bundle == null ? new Bundle() : bundle;
565         if (TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE.equals(type)) {
566             parameters.putString(
567                     TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI,
568                     channelUri == null ? null : channelUri.toString());
569             parameters.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_INPUT_ID, inputId);
570         }
571         mHandler.post(() -> sendPlaybackCommandRequest(type, parameters));
572         // Delay request for new information to give time to tune
573         mHandler.postDelayed(
574                 () -> {
575                     requestCurrentTvInputId();
576                     requestCurrentChannelUri();
577                     requestTrackInfoList();
578                 },
579                 1000
580         );
581     }
582 
tuneChannelByType(String type, String inputId, Uri channelUri)583     private void tuneChannelByType(String type, String inputId, Uri channelUri) {
584         tuneChannelByType(type, inputId, channelUri, new Bundle());
585     }
586 
tuneToNextChannel()587     private void tuneToNextChannel() {
588         tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT, null, null);
589     }
590 
591     @Override
onCurrentChannelUri(Uri channelUri)592     public void onCurrentChannelUri(Uri channelUri) {
593         if (DEBUG) {
594             Log.d(TAG, "onCurrentChannelUri uri=" + channelUri);
595         }
596         mCurrentChannelUri = channelUri;
597         mChannelUriView.setText("Channel URI: " + channelUri);
598     }
599 
600     @Override
onTrackInfoList(List<TvTrackInfo> tracks)601     public void onTrackInfoList(List<TvTrackInfo> tracks) {
602         if (DEBUG) {
603             Log.d(TAG, "onTrackInfoList size=" + tracks.size());
604             for (int i = 0; i < tracks.size(); i++) {
605                 TvTrackInfo trackInfo = tracks.get(i);
606                 if (trackInfo != null) {
607                     Log.d(TAG, "track " + i + ": type=" + trackInfo.getType() +
608                             " id=" + trackInfo.getId());
609                 }
610             }
611         }
612         for (TvTrackInfo info : tracks) {
613             if (info.getType() == TvTrackInfo.TYPE_AUDIO) {
614                 mFirstAudioTrackId = info.getId();
615                 break;
616             }
617         }
618         mTracks = tracks;
619     }
620 
621     @Override
onTracksChanged(List<TvTrackInfo> tracks)622     public void onTracksChanged(List<TvTrackInfo> tracks) {
623         if (DEBUG) {
624             Log.d(TAG, "onTracksChanged");
625         }
626         onTrackInfoList(tracks);
627     }
628 
629     @Override
onTrackSelected(int type, String trackId)630     public void onTrackSelected(int type, String trackId) {
631         if (DEBUG) {
632             Log.d(TAG, "onTrackSelected type=" + type + " trackId=" + trackId);
633         }
634         updateTrackSelectedView(type, trackId);
635 
636         if (TextUtils.equals(mSelectingAudioTrackId, trackId)) {
637             if (mSelectingAudioTrackId == null) {
638                 updateLogText("unselect audio succeed");
639             } else {
640                 updateLogText("select audio succeed");
641             }
642         }
643     }
644 
645     @Override
onCurrentTvInputId(String inputId)646     public void onCurrentTvInputId(String inputId) {
647         if (DEBUG) {
648             Log.d(TAG, "onCurrentTvInputId id=" + inputId);
649         }
650         mCurrentTvInputId = inputId;
651         mTvInputIdView.setText("TV Input ID: " + inputId);
652     }
653 
654     @Override
onTuned(Uri channelUri)655     public void onTuned(Uri channelUri) {
656         mCurrentChannelUri = channelUri;
657     }
658 
659     @Override
onCurrentVideoBounds(@onNull Rect bounds)660     public void onCurrentVideoBounds(@NonNull Rect bounds) {
661         updateLogText("Received video Bounds " + bounds.toShortString());
662     }
663 
664     @Override
onBroadcastInfoResponse(BroadcastInfoResponse response)665     public void onBroadcastInfoResponse(BroadcastInfoResponse response) {
666         if (mGeneratedRequestId == response.getRequestId()) {
667             if (!mRequestStreamEventFinished && response instanceof StreamEventResponse) {
668                 handleStreamEventResponse((StreamEventResponse) response);
669             } else if (mSectionReceived < MAX_HANDLED_RESPONSE
670                     && response instanceof SectionResponse) {
671                 handleSectionResponse((SectionResponse) response);
672             } else if (response instanceof TableResponse) {
673                 handleTableResponse((TableResponse) response);
674             }
675         }
676     }
677 
handleSectionResponse(SectionResponse response)678     private void handleSectionResponse(SectionResponse response) {
679         mSectionReceived++;
680         byte[] data = null;
681         Bundle params = response.getSessionData();
682         if (params != null) {
683             // TODO: define the key
684             data = params.getByteArray("key_raw_data");
685         }
686         int version = response.getVersion();
687         updateLogText(
688                 "Received section data version = "
689                         + version
690                         + ", data = "
691                         + Arrays.toString(data));
692     }
693 
handleStreamEventResponse(StreamEventResponse response)694     private void handleStreamEventResponse(StreamEventResponse response) {
695         updateLogText("Received stream event response");
696         byte[] rData = response.getData();
697         if (rData == null) {
698             mRequestStreamEventFinished = true;
699             updateLogText("Received stream event data is null");
700             return;
701         }
702         // TODO: convert to Hex instead
703         String data = Arrays.toString(rData);
704         if (mStreamDataList.contains(data)) {
705             return;
706         }
707         mStreamDataList.add(data);
708         updateLogText(
709                 "Received stream event data("
710                         + (mStreamDataList.size() - 1)
711                         + "): "
712                         + data);
713         if (mStreamDataList.size() >= MAX_HANDLED_RESPONSE) {
714             mRequestStreamEventFinished = true;
715             updateLogText("Received stream event data finished");
716         }
717     }
718 
handleTableResponse(TableResponse response)719     private void handleTableResponse(TableResponse response) {
720         updateLogText(
721                 "Received table data version = "
722                         + response.getVersion()
723                         + ", size="
724                         + response.getSize()
725                         + ", requestId="
726                         + response.getRequestId()
727                         + ", data = "
728                         + Arrays.toString(getTableByteArray(response)));
729     }
730 
selectTrack(int type, String trackId)731     private void selectTrack(int type, String trackId) {
732         Bundle params = new Bundle();
733         params.putInt(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, type);
734         params.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, trackId);
735         mHandler.post(
736                 () ->
737                         sendPlaybackCommandRequest(
738                                 TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK,
739                                 params));
740     }
741 
generateRequestId()742     private int generateRequestId() {
743         return ++mGeneratedRequestId;
744     }
745 
requestStreamEvent(String targetUri, String eventName)746     private void requestStreamEvent(String targetUri, String eventName) {
747         if (targetUri == null) {
748             return;
749         }
750         int requestId = generateRequestId();
751         BroadcastInfoRequest request =
752                 new StreamEventRequest(
753                         requestId,
754                         BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
755                         Uri.parse(targetUri),
756                         eventName);
757         requestBroadcastInfo(request);
758     }
759 
requestSection(boolean repeat, int tsPid, int tableId, int version)760     private void requestSection(boolean repeat, int tsPid, int tableId, int version) {
761         int requestId = generateRequestId();
762         BroadcastInfoRequest request =
763                 new SectionRequest(
764                         requestId,
765                         repeat ?
766                                 BroadcastInfoRequest.REQUEST_OPTION_REPEAT :
767                                 BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
768                         tsPid,
769                         tableId,
770                         version);
771         requestBroadcastInfo(request);
772     }
773 
requestTable(boolean repeat, int tableId, int tableName, int version)774     private void requestTable(boolean repeat,  int tableId, int tableName, int version) {
775         int requestId = generateRequestId();
776         BroadcastInfoRequest request =
777                 new TableRequest(
778                         requestId,
779                         repeat
780                                 ? BroadcastInfoRequest.REQUEST_OPTION_REPEAT
781                                 : BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
782                         tableId,
783                         tableName,
784                         version);
785         requestBroadcastInfo(request);
786     }
787 
requestAd()788     public void requestAd() {
789         try {
790             // TODO: add the AD file to this project
791             RandomAccessFile adiFile =
792                     new RandomAccessFile(
793                             mContext.getApplicationContext().getFilesDir() + "/ad.mp4", "r");
794             mAdFd = ParcelFileDescriptor.dup(adiFile.getFD());
795         } catch (Exception e) {
796             updateLogText("open advertisement file failed. " + e.getMessage());
797             return;
798         }
799         long startTime = 20000;
800         long stopTime = startTime + 25000;
801         long echoInterval = 1000;
802         String mediaFileType = "MP4";
803         mHandler.post(
804                 () -> {
805                     AdRequest adRequest;
806                     if (mAdState == AdResponse.RESPONSE_TYPE_PLAYING) {
807                         updateLogText("RequestAd stop");
808                         adRequest =
809                                 new AdRequest(
810                                         mGeneratedRequestId,
811                                         AdRequest.REQUEST_TYPE_STOP,
812                                         null,
813                                         0,
814                                         0,
815                                         0,
816                                         null,
817                                         null);
818                     } else {
819                         updateLogText("RequestAd start");
820                         int requestId = generateRequestId();
821                         mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
822                         mAdSurfaceView.setVisibility(View.VISIBLE);
823                         Bundle bundle = new Bundle();
824                         bundle.putParcelable("dai_surface", mAdSurface);
825                         adRequest =
826                                 new AdRequest(
827                                         requestId,
828                                         AdRequest.REQUEST_TYPE_START,
829                                         mAdFd,
830                                         startTime,
831                                         stopTime,
832                                         echoInterval,
833                                         mediaFileType,
834                                         bundle);
835                     }
836                     requestAd(adRequest);
837                 });
838     }
839 
840     @TargetApi(34)
getAppLinkInfoList()841     private List<AppLinkInfo> getAppLinkInfoList() {
842         if (Build.VERSION.SDK_INT < 34 || mTvIAppManager == null) {
843             return new ArrayList<>();
844         }
845         return mTvIAppManager.getAppLinkInfoList();
846     }
847 
848     @TargetApi(34)
requestCurrentVideoBoundsWrapper()849     private void requestCurrentVideoBoundsWrapper() {
850         if (Build.VERSION.SDK_INT < 34) {
851             return;
852         }
853         requestCurrentVideoBounds();
854     }
855 
856     @TargetApi(34)
getTableByteArray(TableResponse response)857     private byte[] getTableByteArray(TableResponse response) {
858         if (Build.VERSION.SDK_INT < 34) {
859             return null;
860         }
861         return response.getTableByteArray();
862     }
863 }
864