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.interactive; 18 19 import static com.android.tv.util.CaptionSettings.OPTION_OFF; 20 import static com.android.tv.util.CaptionSettings.OPTION_ON; 21 22 import android.annotation.TargetApi; 23 import android.graphics.Rect; 24 import android.media.tv.TvTrackInfo; 25 import android.media.tv.interactive.TvInteractiveAppManager; 26 import android.media.tv.AitInfo; 27 import android.media.tv.interactive.TvInteractiveAppService; 28 import android.media.tv.interactive.TvInteractiveAppServiceInfo; 29 import android.media.tv.interactive.TvInteractiveAppView; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.support.annotation.NonNull; 35 import android.util.Log; 36 import android.view.InputEvent; 37 import android.view.KeyEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 41 import com.android.tv.MainActivity; 42 import com.android.tv.R; 43 import com.android.tv.common.SoftPreconditions; 44 import com.android.tv.common.util.ContentUriUtils; 45 import com.android.tv.data.api.Channel; 46 import com.android.tv.dialog.InteractiveAppDialogFragment; 47 import com.android.tv.features.TvFeatures; 48 import com.android.tv.ui.TunableTvView; 49 import com.android.tv.util.TvSettings; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.concurrent.ExecutorService; 54 import java.util.concurrent.Executors; 55 56 @TargetApi(Build.VERSION_CODES.TIRAMISU) 57 public class IAppManager { 58 private static final String TAG = "IAppManager"; 59 private static final boolean DEBUG = false; 60 61 private final MainActivity mMainActivity; 62 private final TvInteractiveAppManager mTvIAppManager; 63 private final TvInteractiveAppView mTvIAppView; 64 private final TunableTvView mTvView; 65 private final Handler mHandler; 66 private AitInfo mCurrentAitInfo; 67 private AitInfo mHeldAitInfo; // AIT info that has been held pending dialog confirmation 68 private boolean mTvAppDialogShown = false; 69 IAppManager(@onNull MainActivity parentActivity, @NonNull TunableTvView tvView, @NonNull Handler handler)70 public IAppManager(@NonNull MainActivity parentActivity, @NonNull TunableTvView tvView, 71 @NonNull Handler handler) { 72 SoftPreconditions.checkFeatureEnabled(parentActivity, TvFeatures.HAS_TIAF, TAG); 73 74 mMainActivity = parentActivity; 75 mTvView = tvView; 76 mHandler = handler; 77 mTvIAppManager = mMainActivity.getSystemService(TvInteractiveAppManager.class); 78 mTvIAppView = mMainActivity.findViewById(R.id.tv_app_view); 79 if (mTvIAppManager == null || mTvIAppView == null) { 80 Log.e(TAG, "Could not find interactive app view or manager"); 81 return; 82 } 83 84 ExecutorService executor = Executors.newSingleThreadExecutor(); 85 mTvIAppManager.registerCallback( 86 executor, 87 new MyInteractiveAppManagerCallback() 88 ); 89 mTvIAppView.setCallback( 90 executor, 91 new MyInteractiveAppViewCallback() 92 ); 93 mTvIAppView.setOnUnhandledInputEventListener(executor, 94 inputEvent -> { 95 if (mMainActivity.isKeyEventBlocked()) { 96 return true; 97 } 98 if (inputEvent instanceof KeyEvent) { 99 KeyEvent keyEvent = (KeyEvent) inputEvent; 100 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN 101 && keyEvent.isLongPress()) { 102 if (mMainActivity.onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { 103 return true; 104 } 105 } 106 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 107 return mMainActivity.onKeyUp(keyEvent.getKeyCode(), keyEvent); 108 } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 109 return mMainActivity.onKeyDown(keyEvent.getKeyCode(), keyEvent); 110 } 111 } 112 return false; 113 }); 114 } 115 stop()116 public void stop() { 117 mTvIAppView.stopInteractiveApp(); 118 mTvIAppView.reset(); 119 mCurrentAitInfo = null; 120 } 121 122 /* 123 * Update current info based on ait info that was held when the dialog was shown. 124 */ processHeldAitInfo()125 public void processHeldAitInfo() { 126 if (mHeldAitInfo != null) { 127 onAitInfoUpdated(mHeldAitInfo); 128 } 129 } 130 dispatchKeyEvent(KeyEvent event)131 public boolean dispatchKeyEvent(KeyEvent event) { 132 if (mTvIAppView != null && mTvIAppView.getVisibility() == View.VISIBLE 133 && mTvIAppView.dispatchKeyEvent(event)){ 134 return true; 135 } 136 return false; 137 } 138 onAitInfoUpdated(AitInfo aitInfo)139 public void onAitInfoUpdated(AitInfo aitInfo) { 140 if (mTvIAppManager == null || aitInfo == null) { 141 return; 142 } 143 if (mCurrentAitInfo != null && mCurrentAitInfo.getType() == aitInfo.getType()) { 144 if (DEBUG) { 145 Log.d(TAG, "Ignoring AIT update: Same type as current"); 146 } 147 return; 148 } 149 150 List<TvInteractiveAppServiceInfo> tvIAppInfoList = 151 mTvIAppManager.getTvInteractiveAppServiceList(); 152 if (tvIAppInfoList.isEmpty()) { 153 if (DEBUG) { 154 Log.d(TAG, "Ignoring AIT update: No interactive app services registered"); 155 } 156 return; 157 } 158 159 // App Type ID numbers allocated by DVB Services 160 int type = -1; 161 switch (aitInfo.getType()) { 162 case 0x0010: // HBBTV 163 type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_HBBTV; 164 break; 165 case 0x0006: // DCAP-J: DCAP Java applications 166 case 0x0007: // DCAP-X: DCAP XHTML applications 167 type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_ATSC; 168 break; 169 case 0x0001: // Ginga-J 170 case 0x0009: // Ginga-NCL 171 case 0x000b: // Ginga-HTML5 172 type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_GINGA; 173 break; 174 default: 175 Log.e(TAG, "AIT info contained unknown type: " + aitInfo.getType()); 176 return; 177 } 178 179 if (TvSettings.isTvIAppOn(mMainActivity.getApplicationContext())) { 180 mTvAppDialogShown = false; 181 for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { 182 if ((info.getSupportedTypes() & type) > 0) { 183 mCurrentAitInfo = aitInfo; 184 if (mTvIAppView != null) { 185 mTvIAppView.setVisibility(View.VISIBLE); 186 mTvIAppView.prepareInteractiveApp(info.getId(), type); 187 } 188 break; 189 } 190 } 191 } else if (!mTvAppDialogShown) { 192 if (DEBUG) { 193 Log.d(TAG, "TV IApp is not enabled"); 194 } 195 196 for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { 197 if ((info.getSupportedTypes() & type) > 0) { 198 mMainActivity.getOverlayManager().showDialogFragment( 199 InteractiveAppDialogFragment.DIALOG_TAG, 200 InteractiveAppDialogFragment.create(info.getServiceInfo().packageName), 201 false); 202 mHeldAitInfo = aitInfo; 203 mTvAppDialogShown = true; 204 break; 205 } 206 } 207 } 208 } 209 210 private class MyInteractiveAppManagerCallback extends 211 TvInteractiveAppManager.TvInteractiveAppCallback { 212 @Override onInteractiveAppServiceAdded(String iAppServiceId)213 public void onInteractiveAppServiceAdded(String iAppServiceId) {} 214 215 @Override onInteractiveAppServiceRemoved(String iAppServiceId)216 public void onInteractiveAppServiceRemoved(String iAppServiceId) {} 217 218 @Override onInteractiveAppServiceUpdated(String iAppServiceId)219 public void onInteractiveAppServiceUpdated(String iAppServiceId) {} 220 221 @Override onTvInteractiveAppServiceStateChanged(String iAppServiceId, int type, int state, int err)222 public void onTvInteractiveAppServiceStateChanged(String iAppServiceId, int type, int state, 223 int err) { 224 if (state == TvInteractiveAppManager.SERVICE_STATE_READY && mTvIAppView != null) { 225 mTvIAppView.startInteractiveApp(); 226 mTvIAppView.setTvView(mTvView.getTvView()); 227 if (mTvView.getTvView() != null) { 228 mTvView.getTvView().setInteractiveAppNotificationEnabled(true); 229 } 230 } 231 } 232 } 233 234 private class MyInteractiveAppViewCallback extends 235 TvInteractiveAppView.TvInteractiveAppCallback { 236 @Override onPlaybackCommandRequest(String iAppServiceId, String cmdType, Bundle parameters)237 public void onPlaybackCommandRequest(String iAppServiceId, String cmdType, 238 Bundle parameters) { 239 if (mTvView == null || cmdType == null) { 240 return; 241 } 242 switch (cmdType) { 243 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE: 244 if (parameters == null) { 245 return; 246 } 247 String uriString = parameters.getString( 248 TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI); 249 if (uriString != null) { 250 Uri channelUri = Uri.parse(uriString); 251 Channel channel = mMainActivity.getChannelDataManager().getChannel( 252 ContentUriUtils.safeParseId(channelUri)); 253 if (channel != null) { 254 mHandler.post(() -> mMainActivity.tuneToChannel(channel)); 255 } 256 } 257 break; 258 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK: 259 if (mTvView != null && parameters != null) { 260 int trackType = parameters.getInt( 261 TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, 262 -1); 263 String trackId = parameters.getString( 264 TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, 265 null); 266 switch (trackType) { 267 case TvTrackInfo.TYPE_AUDIO: 268 // When trackId is null, deselects current audio track. 269 mHandler.post(() -> mMainActivity.selectAudioTrack(trackId)); 270 break; 271 case TvTrackInfo.TYPE_SUBTITLE: 272 // When trackId is null, turns off captions. 273 mHandler.post(() -> mMainActivity.selectSubtitleTrack( 274 trackId == null ? OPTION_OFF : OPTION_ON, trackId)); 275 break; 276 } 277 } 278 break; 279 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME: 280 if (parameters == null) { 281 return; 282 } 283 float volume = parameters.getFloat( 284 TvInteractiveAppService.COMMAND_PARAMETER_KEY_VOLUME, -1); 285 if (volume >= 0.0 && volume <= 1.0) { 286 mHandler.post(() -> mTvView.setStreamVolume(volume)); 287 } 288 break; 289 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT: 290 mHandler.post(mMainActivity::channelUp); 291 break; 292 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_PREV: 293 mHandler.post(mMainActivity::channelDown); 294 break; 295 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP: 296 int mode = 1; // TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK 297 if (parameters != null) { 298 mode = parameters.getInt( 299 /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */ 300 "command_stop_mode", 301 /*TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK*/ 302 1); 303 } 304 mHandler.post(mMainActivity::stopTv); 305 break; 306 default: 307 Log.e(TAG, "PlaybackCommandRequest had unknown cmdType:" 308 + cmdType); 309 break; 310 } 311 } 312 313 @Override onStateChanged(String iAppServiceId, int state, int err)314 public void onStateChanged(String iAppServiceId, int state, int err) { 315 } 316 317 @Override onBiInteractiveAppCreated(String iAppServiceId, Uri biIAppUri, String biIAppId)318 public void onBiInteractiveAppCreated(String iAppServiceId, Uri biIAppUri, 319 String biIAppId) {} 320 321 @Override onTeletextAppStateChanged(String iAppServiceId, int state)322 public void onTeletextAppStateChanged(String iAppServiceId, int state) {} 323 324 @Override onSetVideoBounds(String iAppServiceId, Rect rect)325 public void onSetVideoBounds(String iAppServiceId, Rect rect) { 326 if (mTvView != null) { 327 ViewGroup.MarginLayoutParams layoutParams = mTvView.getTvViewLayoutParams(); 328 layoutParams.setMargins(rect.left, rect.top, rect.right, rect.bottom); 329 mTvView.setTvViewLayoutParams(layoutParams); 330 } 331 } 332 333 @Override 334 @TargetApi(34) onRequestCurrentVideoBounds(@onNull String iAppServiceId)335 public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) { 336 mHandler.post( 337 () -> { 338 if (DEBUG) { 339 Log.d(TAG, "onRequestCurrentVideoBounds service ID = " 340 + iAppServiceId); 341 } 342 Rect bounds = new Rect(mTvView.getLeft(), mTvView.getTop(), 343 mTvView.getRight(), mTvView.getBottom()); 344 mTvIAppView.sendCurrentVideoBounds(bounds); 345 }); 346 } 347 348 @Override onRequestCurrentChannelUri(String iAppServiceId)349 public void onRequestCurrentChannelUri(String iAppServiceId) { 350 if (mTvIAppView == null) { 351 return; 352 } 353 Channel currentChannel = mMainActivity.getCurrentChannel(); 354 Uri currentUri = (currentChannel == null) 355 ? null 356 : currentChannel.getUri(); 357 mTvIAppView.sendCurrentChannelUri(currentUri); 358 } 359 360 @Override onRequestCurrentChannelLcn(String iAppServiceId)361 public void onRequestCurrentChannelLcn(String iAppServiceId) { 362 if (mTvIAppView == null) { 363 return; 364 } 365 Channel currentChannel = mMainActivity.getCurrentChannel(); 366 if (currentChannel == null || currentChannel.getDisplayNumber() == null) { 367 return; 368 } 369 // Expected format is major channel number, delimiter, minor channel number 370 String displayNumber = currentChannel.getDisplayNumber(); 371 String format = "[0-9]+" + Channel.CHANNEL_NUMBER_DELIMITER + "[0-9]+"; 372 if (!displayNumber.matches(format)) { 373 return; 374 } 375 // Major channel number is returned 376 String[] numbers = displayNumber.split( 377 String.valueOf(Channel.CHANNEL_NUMBER_DELIMITER)); 378 mTvIAppView.sendCurrentChannelLcn(Integer.parseInt(numbers[0])); 379 } 380 381 @Override onRequestStreamVolume(String iAppServiceId)382 public void onRequestStreamVolume(String iAppServiceId) { 383 if (mTvIAppView == null || mTvView == null) { 384 return; 385 } 386 mTvIAppView.sendStreamVolume(mTvView.getStreamVolume()); 387 } 388 389 @Override onRequestTrackInfoList(String iAppServiceId)390 public void onRequestTrackInfoList(String iAppServiceId) { 391 if (mTvIAppView == null || mTvView == null) { 392 return; 393 } 394 List<TvTrackInfo> allTracks = new ArrayList<>(); 395 int[] trackTypes = new int[] {TvTrackInfo.TYPE_AUDIO, 396 TvTrackInfo.TYPE_VIDEO, TvTrackInfo.TYPE_SUBTITLE}; 397 398 for (int trackType : trackTypes) { 399 List<TvTrackInfo> currentTracks = mTvView.getTracks(trackType); 400 if (currentTracks == null) { 401 continue; 402 } 403 for (TvTrackInfo track : currentTracks) { 404 if (track != null) { 405 allTracks.add(track); 406 } 407 } 408 } 409 mTvIAppView.sendTrackInfoList(allTracks); 410 } 411 412 @Override onRequestCurrentTvInputId(String iAppServiceId)413 public void onRequestCurrentTvInputId(String iAppServiceId) { 414 if (mTvIAppView == null) { 415 return; 416 } 417 Channel currentChannel = mMainActivity.getCurrentChannel(); 418 String currentInputId = (currentChannel == null) 419 ? null 420 : currentChannel.getInputId(); 421 mTvIAppView.sendCurrentTvInputId(currentInputId); 422 } 423 424 @Override onRequestSigning(String iAppServiceId, String signingId, String algorithm, String alias, byte[] data)425 public void onRequestSigning(String iAppServiceId, String signingId, String algorithm, 426 String alias, byte[] data) {} 427 } 428 } 429