[LiveTv] Implement more TIAF features in LiveTv and sample TIAS

Bug: 281779518
Test: mmm
Change-Id: I64932aec6a9b634cacaa390a36a3cf35a478612b
diff --git a/interactive/SampleTvInteractiveAppService/AndroidManifest.xml b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml
index d631b95..72cd22f 100644
--- a/interactive/SampleTvInteractiveAppService/AndroidManifest.xml
+++ b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml
@@ -21,6 +21,7 @@
 
     <uses-permission android:name="com.google.android.dtvprovider.permission.READ" />
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
 
     <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
     <uses-feature android:name="android.software.leanback" android:required="false" />
diff --git a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java
index 0f18522..d85ab77 100644
--- a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java
+++ b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java
@@ -16,39 +16,54 @@
 
 package com.android.tv.samples.sampletvinteractiveappservice;
 
+import android.annotation.TargetApi;
 import android.app.Presentation;
 import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.media.MediaPlayer;
+import android.media.tv.AdRequest;
+import android.media.tv.AdResponse;
 import android.media.tv.BroadcastInfoRequest;
 import android.media.tv.BroadcastInfoResponse;
 import android.media.tv.SectionRequest;
 import android.media.tv.SectionResponse;
 import android.media.tv.StreamEventRequest;
 import android.media.tv.StreamEventResponse;
+import android.media.tv.TableRequest;
+import android.media.tv.TableResponse;
 import android.media.tv.TvTrackInfo;
+import android.media.tv.interactive.AppLinkInfo;
 import android.media.tv.interactive.TvInteractiveAppManager;
 import android.media.tv.interactive.TvInteractiveAppService;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.widget.VideoView;
 
 import androidx.annotation.NonNull;
 
+import java.io.RandomAccessFile;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -63,6 +78,7 @@
     private static final int MAX_HANDLED_RESPONSE = 3;
 
     private final Context mContext;
+    private TvInteractiveAppManager mTvIAppManager;
     private final Handler mHandler;
     private final String mAppServiceId;
     private final int mType;
@@ -79,6 +95,11 @@
     private TextView mLogView;
 
     private VideoView mVideoView;
+    private SurfaceView mAdSurfaceView;
+    private Surface mAdSurface;
+    private ParcelFileDescriptor mAdFd;
+    private FrameLayout mMediaContainer;
+    private int mAdState;
     private int mWidth;
     private int mHeight;
     private int mScreenWidth;
@@ -103,12 +124,68 @@
         mAppServiceId = iAppServiceId;
         mType = type;
         mHandler = new Handler(context.getMainLooper());
+        mTvIAppManager = (TvInteractiveAppManager) mContext.getSystemService(
+                Context.TV_INTERACTIVE_APP_SERVICE);
 
         mViewContainer = new LinearLayout(context);
         mViewContainer.setBackground(new ColorDrawable(0));
     }
 
     @Override
+    public View onCreateMediaView() {
+        mAdSurfaceView = new SurfaceView(mContext);
+        if (DEBUG) {
+            Log.d(TAG, "create surfaceView");
+        }
+        mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+        mAdSurfaceView
+                .getHolder()
+                .addCallback(
+                        new SurfaceHolder.Callback() {
+                            @Override
+                            public void surfaceCreated(SurfaceHolder holder) {
+                                mAdSurface = holder.getSurface();
+                            }
+
+                            @Override
+                            public void surfaceChanged(
+                                    SurfaceHolder holder, int format, int width, int height) {
+                                mAdSurface = holder.getSurface();
+                            }
+
+                            @Override
+                            public void surfaceDestroyed(SurfaceHolder holder) {}
+                        });
+        mAdSurfaceView.setVisibility(View.INVISIBLE);
+        ViewGroup.LayoutParams layoutParams =
+                new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+        mAdSurfaceView.setLayoutParams(layoutParams);
+        mMediaContainer.addView(mVideoView);
+        mMediaContainer.addView(mAdSurfaceView);
+        return mMediaContainer;
+    }
+
+    @Override
+    public void onAdResponse(AdResponse adResponse) {
+        mAdState = adResponse.getResponseType();
+        switch (mAdState) {
+            case AdResponse.RESPONSE_TYPE_PLAYING:
+                long time = adResponse.getElapsedTimeMillis();
+                updateLogText("AD is playing. " + time);
+                break;
+            case AdResponse.RESPONSE_TYPE_STOPPED:
+                updateLogText("AD is stopped.");
+                mAdSurfaceView.setVisibility(View.INVISIBLE);
+                break;
+            case AdResponse.RESPONSE_TYPE_FINISHED:
+                updateLogText("AD is play finished.");
+                mAdSurfaceView.setVisibility(View.INVISIBLE);
+                break;
+        }
+    }
+
+    @Override
     public void onRelease() {
         if (DEBUG) {
             Log.d(TAG, "onRelease");
@@ -270,6 +347,65 @@
                 mSectionReceived = 0;
                 requestSection(false, 0, 0x0, -1);
                 return true;
+            case KeyEvent.KEYCODE_I:
+                if (mTvIAppManager == null) {
+                    updateLogText("TvIAppManager null");
+                    return false;
+                }
+                List<AppLinkInfo> appLinks = getAppLinkInfoList();
+                if (appLinks.isEmpty()) {
+                    updateLogText("Not found AppLink");
+                } else {
+                    AppLinkInfo appLink = appLinks.get(0);
+                    Intent intent = new Intent();
+                    intent.setComponent(appLink.getComponentName());
+                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    mContext.getApplicationContext().startActivity(intent);
+                    updateLogText("Launch " + appLink.getComponentName());
+                }
+                return true;
+            case KeyEvent.KEYCODE_J:
+                updateLogText("Request SI Tables ");
+                // Network Information Table (NIT)
+                requestTable(false, 0x40, /* TableRequest.TABLE_NAME_NIT */ 3, -1);
+                // Service Description Table (SDT)
+                requestTable(false, 0x42, /* TableRequest.TABLE_NAME_SDT */ 5, -1);
+                // Event Information Table (EIT)
+                requestTable(false, 0x4e, /* TableRequest.TABLE_NAME_EIT */ 6, -1);
+                return true;
+            case KeyEvent.KEYCODE_K:
+                updateLogText("Request Video Bounds");
+                requestCurrentVideoBoundsWrapper();
+                return true;
+            case KeyEvent.KEYCODE_L: {
+                updateLogText("stop video broadcast with blank mode");
+                Bundle params = new Bundle();
+                params.putInt(
+                        /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+                        "command_stop_mode",
+                        /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK */
+                        1);
+                tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+                        mCurrentTvInputId, null, params);
+                return true;
+            }
+            case KeyEvent.KEYCODE_M: {
+                updateLogText("stop video broadcast with freeze mode");
+                Bundle params = new Bundle();
+                params.putInt(
+                        /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+                        "command_stop_mode",
+                        /* TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_FREEZE */
+                        2);
+                tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP,
+                        mCurrentTvInputId, null, params);
+                return true;
+            }
+            case KeyEvent.KEYCODE_N: {
+                updateLogText("request AD");
+                requestAd();
+                return true;
+            }
             default:
                 return super.onKeyDown(keyCode, event);
         }
@@ -287,6 +423,12 @@
             case KeyEvent.KEYCODE_F:
             case KeyEvent.KEYCODE_G:
             case KeyEvent.KEYCODE_H:
+            case KeyEvent.KEYCODE_I:
+            case KeyEvent.KEYCODE_J:
+            case KeyEvent.KEYCODE_K:
+            case KeyEvent.KEYCODE_L:
+            case KeyEvent.KEYCODE_M:
+            case KeyEvent.KEYCODE_N:
                 return true;
             default:
                 return super.onKeyUp(keyCode, event);
@@ -418,8 +560,8 @@
         );
     }
 
-    private void tuneChannelByType(String type, String inputId, Uri channelUri) {
-        Bundle parameters = new Bundle();
+    private void tuneChannelByType(String type, String inputId, Uri channelUri, Bundle bundle) {
+        Bundle parameters = bundle == null ? new Bundle() : bundle;
         if (TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE.equals(type)) {
             parameters.putString(
                     TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI,
@@ -438,6 +580,10 @@
         );
     }
 
+    private void tuneChannelByType(String type, String inputId, Uri channelUri) {
+        tuneChannelByType(type, inputId, channelUri, new Bundle());
+    }
+
     private void tuneToNextChannel() {
         tuneChannelByType(TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT, null, null);
     }
@@ -486,6 +632,14 @@
             Log.d(TAG, "onTrackSelected type=" + type + " trackId=" + trackId);
         }
         updateTrackSelectedView(type, trackId);
+
+        if (TextUtils.equals(mSelectingAudioTrackId, trackId)) {
+            if (mSelectingAudioTrackId == null) {
+                updateLogText("unselect audio succeed");
+            } else {
+                updateLogText("select audio succeed");
+            }
+        }
     }
 
     @Override
@@ -503,6 +657,11 @@
     }
 
     @Override
+    public void onCurrentVideoBounds(@NonNull Rect bounds) {
+        updateLogText("Received video Bounds " + bounds.toShortString());
+    }
+
+    @Override
     public void onBroadcastInfoResponse(BroadcastInfoResponse response) {
         if (mGeneratedRequestId == response.getRequestId()) {
             if (!mRequestStreamEventFinished && response instanceof StreamEventResponse) {
@@ -510,6 +669,8 @@
             } else if (mSectionReceived < MAX_HANDLED_RESPONSE
                     && response instanceof SectionResponse) {
                 handleSectionResponse((SectionResponse) response);
+            } else if (response instanceof TableResponse) {
+                handleTableResponse((TableResponse) response);
             }
         }
     }
@@ -555,6 +716,18 @@
         }
     }
 
+    private void handleTableResponse(TableResponse response) {
+        updateLogText(
+                "Received table data version = "
+                        + response.getVersion()
+                        + ", size="
+                        + response.getSize()
+                        + ", requestId="
+                        + response.getRequestId()
+                        + ", data = "
+                        + Arrays.toString(getTableByteArray(response)));
+    }
+
     private void selectTrack(int type, String trackId) {
         Bundle params = new Bundle();
         params.putInt(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, type);
@@ -570,7 +743,7 @@
         return ++mGeneratedRequestId;
     }
 
-    public void requestStreamEvent(String targetUri, String eventName) {
+    private void requestStreamEvent(String targetUri, String eventName) {
         if (targetUri == null) {
             return;
         }
@@ -584,7 +757,7 @@
         requestBroadcastInfo(request);
     }
 
-    public void requestSection(boolean repeat, int tsPid, int tableId, int version) {
+    private void requestSection(boolean repeat, int tsPid, int tableId, int version) {
         int requestId = generateRequestId();
         BroadcastInfoRequest request =
                 new SectionRequest(
@@ -597,4 +770,94 @@
                         version);
         requestBroadcastInfo(request);
     }
+
+    private void requestTable(boolean repeat,  int tableId, int tableName, int version) {
+        int requestId = generateRequestId();
+        BroadcastInfoRequest request =
+                new TableRequest(
+                        requestId,
+                        repeat
+                                ? BroadcastInfoRequest.REQUEST_OPTION_REPEAT
+                                : BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE,
+                        tableId,
+                        tableName,
+                        version);
+        requestBroadcastInfo(request);
+    }
+
+    public void requestAd() {
+        try {
+            // TODO: add the AD file to this project
+            RandomAccessFile adiFile =
+                    new RandomAccessFile(
+                            mContext.getApplicationContext().getFilesDir() + "/ad.mp4", "r");
+            mAdFd = ParcelFileDescriptor.dup(adiFile.getFD());
+        } catch (Exception e) {
+            updateLogText("open advertisement file failed. " + e.getMessage());
+            return;
+        }
+        long startTime = 20000;
+        long stopTime = startTime + 25000;
+        long echoInterval = 1000;
+        String mediaFileType = "MP4";
+        mHandler.post(
+                () -> {
+                    AdRequest adRequest;
+                    if (mAdState == AdResponse.RESPONSE_TYPE_PLAYING) {
+                        updateLogText("RequestAd stop");
+                        adRequest =
+                                new AdRequest(
+                                        mGeneratedRequestId,
+                                        AdRequest.REQUEST_TYPE_STOP,
+                                        null,
+                                        0,
+                                        0,
+                                        0,
+                                        null,
+                                        null);
+                    } else {
+                        updateLogText("RequestAd start");
+                        int requestId = generateRequestId();
+                        mAdSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+                        mAdSurfaceView.setVisibility(View.VISIBLE);
+                        Bundle bundle = new Bundle();
+                        bundle.putParcelable("dai_surface", mAdSurface);
+                        adRequest =
+                                new AdRequest(
+                                        requestId,
+                                        AdRequest.REQUEST_TYPE_START,
+                                        mAdFd,
+                                        startTime,
+                                        stopTime,
+                                        echoInterval,
+                                        mediaFileType,
+                                        bundle);
+                    }
+                    requestAd(adRequest);
+                });
+    }
+
+    @TargetApi(34)
+    private List<AppLinkInfo> getAppLinkInfoList() {
+        if (Build.VERSION.SDK_INT < 34 || mTvIAppManager == null) {
+            return new ArrayList<>();
+        }
+        return mTvIAppManager.getAppLinkInfoList();
+    }
+
+    @TargetApi(34)
+    private void requestCurrentVideoBoundsWrapper() {
+        if (Build.VERSION.SDK_INT < 34) {
+            return;
+        }
+        requestCurrentVideoBounds();
+    }
+
+    @TargetApi(34)
+    private byte[] getTableByteArray(TableResponse response) {
+        if (Build.VERSION.SDK_INT < 34) {
+            return null;
+        }
+        return response.getTableByteArray();
+    }
 }
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index d0cfb30..d005cdd 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -184,7 +184,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -260,10 +259,12 @@
 
     static {
         SYSTEM_INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
-        SYSTEM_INTENT_FILTER.addAction(TvInteractiveAppManager.ACTION_APP_LINK_COMMAND);
         SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
         SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_ON);
         SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_TIME_CHANGED);
+        if (Build.VERSION.SDK_INT > 33) { // TIRAMISU
+            SYSTEM_INTENT_FILTER.addAction(TvInteractiveAppManager.ACTION_APP_LINK_COMMAND);
+        }
     }
 
     private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
diff --git a/src/com/android/tv/interactive/IAppManager.java b/src/com/android/tv/interactive/IAppManager.java
index 90a7641..682b35c 100644
--- a/src/com/android/tv/interactive/IAppManager.java
+++ b/src/com/android/tv/interactive/IAppManager.java
@@ -293,6 +293,14 @@
                     mHandler.post(mMainActivity::channelDown);
                     break;
                 case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP:
+                    int mode = 1; // TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK
+                    if (parameters != null) {
+                        mode = parameters.getInt(
+                                /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */
+                                "command_stop_mode",
+                                /*TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK*/
+                                1);
+                    }
                     mHandler.post(mMainActivity::stopTv);
                     break;
                 default:
@@ -323,6 +331,21 @@
         }
 
         @Override
+        @TargetApi(34)
+        public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) {
+            mHandler.post(
+                    () -> {
+                        if (DEBUG) {
+                            Log.d(TAG, "onRequestCurrentVideoBounds service ID = "
+                                    + iAppServiceId);
+                        }
+                        Rect bounds = new Rect(mTvView.getLeft(), mTvView.getTop(),
+                                mTvView.getRight(), mTvView.getBottom());
+                        mTvIAppView.sendCurrentVideoBounds(bounds);
+                    });
+        }
+
+        @Override
         public void onRequestCurrentChannelUri(String iAppServiceId) {
             if (mTvIAppView == null) {
                 return;