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