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