diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:11:38 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 05:11:38 +0000 |
commit | f3fe00e466f67599bec6d97e0ea499e413698285 (patch) | |
tree | 08aaccf8fde3dc86be6785b36e8729cb56e9b47e | |
parent | 3ee528e708cab0d006d4f85401fed50d33b2227d (diff) | |
parent | 489f952460ffe2ef1eb1348b2083bba4de88567f (diff) | |
download | TV-android14-mainline-os-statsd-release.tar.gz |
Snap for 10453563 from 489f952460ffe2ef1eb1348b2083bba4de88567f to mainline-os-statsd-releaseaml_sta_341615000aml_sta_341511040aml_sta_341410000aml_sta_341311010aml_sta_341114000aml_sta_341111000aml_sta_341010020aml_sta_340912000aml_sta_340911000aml_net_341111030android14-mainline-os-statsd-release
Change-Id: I39e2cd2d1692b8ddc587466d0af07fe735a48a6c
54 files changed, 3148 insertions, 553 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 75e2c4d5..6a2d435f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,9 +20,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.tv"> - <uses-sdk android:minSdkVersion="23" - android:targetSdkVersion="29"/> - <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE"/> @@ -30,6 +27,7 @@ <uses-permission android:name="android.permission.HDMI_CEC"/> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS"/> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_TV_LISTINGS"/> @@ -153,6 +151,7 @@ android:resource="@xml/searchable"/> </activity> <activity android:name="com.android.tv.LauncherActivity" + android:exported="false" android:configChanges="keyboard|keyboardHidden" android:theme="@android:style/Theme.Translucent.NoTitleBar"/> <activity android:name="com.android.tv.SetupPassthroughActivity" @@ -166,6 +165,7 @@ </intent-filter> </activity> <activity android:name="com.android.tv.SelectInputActivity" + android:exported="true" android:configChanges="keyboard|keyboardHidden" android:launchMode="singleTask" android:theme="@style/Theme.SelectInputActivity"> @@ -175,6 +175,7 @@ </intent-filter> </activity> <activity android:name="com.android.tv.onboarding.OnboardingActivity" + android:exported="false" android:configChanges="keyboard|keyboardHidden" android:launchMode="singleTop" android:theme="@style/Theme.Setup.GuidedStep"/> @@ -219,14 +220,18 @@ android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep"/> <activity android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity" android:configChanges="keyboard|keyboardHidden" + android:exported="false" android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep"/> <activity android:name="com.android.tv.dvr.ui.DvrSeriesDeletionActivity" android:configChanges="keyboard|keyboardHidden" + android:exported="false" android:theme="@style/Theme.TV.Dvr.Series.Deletion.GuidedStep"/> <activity android:name="com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity" + android:exported="false" android:theme="@style/Theme.TV.dialog.HalfSizedDialog"/> <activity android:name="com.android.tv.dvr.ui.list.DvrSchedulesActivity" android:configChanges="keyboard|keyboardHidden" + android:exported="false" android:theme="@style/Theme.Leanback.Details"/> <activity android:name="com.android.tv.dvr.ui.list.DvrHistoryActivity" android:configChanges="keyboard|keyboardHidden" @@ -236,6 +241,7 @@ <service android:name="com.android.tv.recommendation.NotificationService" android:exported="false"/> <service android:name="com.android.tv.recommendation.ChannelPreviewUpdater$ChannelPreviewUpdateService" + android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE"/> <receiver android:name="com.android.tv.receiver.BootCompletedReceiver" @@ -272,12 +278,14 @@ </intent-filter> </activity> <!-- DVR --> <service android:name="com.android.tv.dvr.recorder.DvrRecordingService" + android:exported="false" android:label="@string/dvr_service_name"/> <receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver" android:exported="false"/> <service android:name="com.android.tv.data.epg.EpgFetchService" + android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE"/> </application> @@ -1,3 +1,2 @@ -nchalko@google.com shubang@google.com quxiangfang@google.com diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java index e59bcd60..bf76c9ce 100644 --- a/common/src/com/android/tv/common/feature/Sdk.java +++ b/common/src/com/android/tv/common/feature/Sdk.java @@ -27,6 +27,8 @@ public final class Sdk { public static final Feature AT_LEAST_O = new AtLeast(VERSION_CODES.O); + public static final Feature AT_LEAST_T = new AtLeast(VERSION_CODES.TIRAMISU); + private static final class AtLeast implements Feature { private final int versionCode; diff --git a/interactive/SampleTvInteractiveAppService/Android.bp b/interactive/SampleTvInteractiveAppService/Android.bp new file mode 100644 index 00000000..eada4ded --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/Android.bp @@ -0,0 +1,49 @@ +// +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_app { + name: "SampleTvInteractiveAppService", + + srcs: ["src/**/*.java"], + optimize: { + enabled: false, + }, + + privileged: true, + product_specific: true, + sdk_version: "system_current", + min_sdk_version: "33", // T + + resource_dirs: [ + "res", + ], + + static_libs: [ + "androidx.leanback_leanback", + ], + + aaptflags: [ + "--version-name", + version_name, + + "--version-code", + version_code, + ], +} diff --git a/interactive/SampleTvInteractiveAppService/AndroidManifest.xml b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml new file mode 100644 index 00000000..72cd22f9 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.tv.samples.sampletvinteractiveappservice" + tools:ignore="MissingLeanbackLauncher"> + + <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" /> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/sample_tias" + android:supportsRtl="true" + android:theme="@style/Theme.Leanback"> + <service + android:name=".SampleTvInteractiveAppService" + android:enabled="true" + android:exported="true" + android:isolatedProcess="false" + android:permission="android.permission.BIND_TV_INTERACTIVE_APP" + android:process=":rte"> + <intent-filter> + <action android:name="android.media.tv.interactive.TvInteractiveAppService" /> + </intent-filter> + <meta-data + android:name="android.media.tv.interactive.app" + android:resource="@xml/tviappservice" /> + </service> + </application> + +</manifest> diff --git a/interactive/SampleTvInteractiveAppService/build.gradle b/interactive/SampleTvInteractiveAppService/build.gradle new file mode 100644 index 00000000..a51bc56a --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 31 + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + applicationId "com.android.tv.samples.sampletvinteractiveappservice" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName rootProject.ext.versionName + } + android.applicationVariants.all { variant -> + variant.outputs.all { + outputFileName = "SampleTvInteractiveAppService-v${defaultConfig.versionName}.apk" + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} +dependencies { + implementation 'androidx.leanback:leanback:1.0.0' +}
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml b/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml new file mode 100644 index 00000000..915c3526 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/layout/sample_layout.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="50dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@color/overlay_background_color"> + + <TextView + android:layout_gravity="center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="20dp" + android:text="@string/overlay_title_string" + android:textColor="@color/overlay_text_color" + android:textSize="32sp"/> + <TextView + style="@style/overlay_text_item" + android:text="@string/red_button_string" + android:textStyle="bold"/> + <Space + android:layout_width="match_parent" + android:layout_height="20dp"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/app_service_id"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/tv_input_id"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/channel_uri"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/video_track_selected"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/audio_track_selected"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/subtitle_track_selected"/> + <TextView + style="@style/overlay_text_item" + android:id="@+id/log_text"/> + </LinearLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp Binary files differnew file mode 100644 index 00000000..c209e78e --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/mipmap-hdpi/ic_launcher.webp diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp Binary files differnew file mode 100644 index 00000000..4f0f1d64 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/mipmap-mdpi/ic_launcher.webp diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 00000000..948a3070 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xhdpi/ic_launcher.webp diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 00000000..28d4b77f --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp b/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 00000000..aa7d6427 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/interactive/SampleTvInteractiveAppService/res/values/colors.xml b/interactive/SampleTvInteractiveAppService/res/values/colors.xml new file mode 100644 index 00000000..d2a0a25a --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/values/colors.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <color name="overlay_background_color">#CCCCCCCC</color> + <color name="overlay_text_color">#FF000000</color> +</resources>
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/res/values/strings.xml b/interactive/SampleTvInteractiveAppService/res/values/strings.xml new file mode 100644 index 00000000..d0c33d7f --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/values/strings.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <string name="sample_tias">SampleTvInteractiveAppService</string> + <string-array name="sub_iapp_service_types"> + <item>hbbtv</item> + <item>ginga</item> + <item>atsc</item> + </string-array> + <string name="overlay_title_string">Sample TV Interactive App Service</string> + <string name="red_button_string">Press the Red Interactive Button to tune to the next channel</string> +</resources>
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/res/values/styles.xml b/interactive/SampleTvInteractiveAppService/res/values/styles.xml new file mode 100644 index 00000000..d207c99e --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/values/styles.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="overlay_text_item"> + <item name="android:layout_gravity">left</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginLeft">25dp</item> + <item name="android:layout_marginRight">25dp</item> + <item name="android:layout_marginBottom">5dp</item> + <item name="android:textColor">@color/overlay_text_color</item> + <item name="android:textSize">20sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml b/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml new file mode 100644 index 00000000..87020f26 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/res/xml/tviappservice.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<tv-interactive-app xmlns:android="http://schemas.android.com/apk/res/android" + android:supportedTypes="@array/sub_iapp_service_types" />
\ No newline at end of file diff --git a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java new file mode 100644 index 00000000..c53748eb --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/SampleTvInteractiveAppService.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.samples.sampletvinteractiveappservice; + +import android.media.tv.interactive.TvInteractiveAppService; +import android.util.Log; + +public class SampleTvInteractiveAppService extends TvInteractiveAppService { + private static final String TAG = "SampleTvInteractiveAppService"; + private static final boolean DEBUG = true; + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public Session onCreateSession(String iAppServiceId, int type) { + if (DEBUG) { + Log.d(TAG, "onCreateSession iAppServiceId=" + iAppServiceId + "type=" + type); + } + TiasSessionImpl session = new TiasSessionImpl(this, iAppServiceId, type); + session.prepare(this); + return session; + } +} diff --git a/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java new file mode 100644 index 00000000..d85ab776 --- /dev/null +++ b/interactive/SampleTvInteractiveAppService/src/com/android/tv/samples/sampletvinteractiveappservice/TiasSessionImpl.java @@ -0,0 +1,863 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + +public class TiasSessionImpl extends TvInteractiveAppService.Session { + private static final String TAG = "SampleTvInteractiveAppService"; + private static final boolean DEBUG = true; + + private static final String VIRTUAL_DISPLAY_NAME = "sample_tias_display"; + + // For testing purposes, limit the number of response for a single request + 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; + private final ViewGroup mViewContainer; + private Surface mSurface; + private VirtualDisplay mVirtualDisplay; + private List<TvTrackInfo> mTracks; + + private TextView mTvInputIdView; + private TextView mChannelUriView; + private TextView mVideoTrackView; + private TextView mAudioTrackView; + private TextView mSubtitleTrackView; + 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; + private int mScreenHeight; + private String mCurrentTvInputId; + private Uri mCurrentChannelUri; + private String mSelectingAudioTrackId; + private String mFirstAudioTrackId; + private int mGeneratedRequestId = 0; + private boolean mRequestStreamEventFinished = false; + private int mSectionReceived = 0; + private List<String> mStreamDataList = new ArrayList<>(); + private boolean mIsFullScreen = true; + + public TiasSessionImpl(Context context, String iAppServiceId, int type) { + super(context); + if (DEBUG) { + Log.d(TAG, "Constructing service with iAppServiceId=" + iAppServiceId + + " type=" + type); + } + mContext = context; + 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"); + } + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + mVirtualDisplay = null; + } + } + + @Override + public boolean onSetSurface(Surface surface) { + if (DEBUG) { + Log.d(TAG, "onSetSurface"); + } + if (mSurface != null) { + mSurface.release(); + } + updateSurface(surface, mWidth, mHeight); + mSurface = surface; + return true; + } + + @Override + public void onSurfaceChanged(int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "onSurfaceChanged format=" + format + " width=" + width + + " height=" + height); + } + if (mSurface != null) { + updateSurface(mSurface, width, height); + mWidth = width; + mHeight = height; + } + } + + @Override + public void onStartInteractiveApp() { + if (DEBUG) { + Log.d(TAG, "onStartInteractiveApp"); + } + mHandler.post( + () -> { + initSampleView(); + setMediaViewEnabled(true); + requestCurrentTvInputId(); + requestCurrentChannelUri(); + requestTrackInfoList(); + } + ); + } + + @Override + public void onStopInteractiveApp() { + if (DEBUG) { + Log.d(TAG, "onStopInteractiveApp"); + } + } + + public void prepare(TvInteractiveAppService serviceCaller) { + // Slightly delay our post to ensure the Manager has had time to register our Session + mHandler.postDelayed( + () -> { + if (serviceCaller != null) { + serviceCaller.notifyStateChanged(mType, + TvInteractiveAppManager.SERVICE_STATE_READY, + TvInteractiveAppManager.ERROR_NONE); + } + }, + 100); + } + + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + // TODO: use a menu view instead of key events for the following tests + switch (keyCode) { + case KeyEvent.KEYCODE_PROG_RED: + tuneToNextChannel(); + return true; + case KeyEvent.KEYCODE_A: + updateLogText("stop video broadcast begin"); + tuneChannelByType( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP, + mCurrentTvInputId, + null); + updateLogText("stop video broadcast end"); + return true; + case KeyEvent.KEYCODE_B: + updateLogText("resume video broadcast begin"); + tuneChannelByType( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE, + mCurrentTvInputId, + mCurrentChannelUri); + updateLogText("resume video broadcast end"); + return true; + case KeyEvent.KEYCODE_C: + updateLogText("unselect audio track"); + mSelectingAudioTrackId = null; + selectTrack(TvTrackInfo.TYPE_AUDIO, null); + return true; + case KeyEvent.KEYCODE_D: + updateLogText("select audio track " + mFirstAudioTrackId); + mSelectingAudioTrackId = mFirstAudioTrackId; + selectTrack(TvTrackInfo.TYPE_AUDIO, mFirstAudioTrackId); + return true; + case KeyEvent.KEYCODE_E: + if (mVideoView != null) { + if (mVideoView.isPlaying()) { + updateLogText("stop media"); + mVideoView.stopPlayback(); + mVideoView.setVisibility(View.GONE); + tuneChannelByType( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE, + mCurrentTvInputId, + mCurrentChannelUri); + } else { + updateLogText("play media"); + tuneChannelByType( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP, + mCurrentTvInputId, + null); + mVideoView.setVisibility(View.VISIBLE); + // TODO: put a file sample.mp4 in res/raw/ and use R.raw.sample for the URI + Uri uri = Uri.parse( + "android.resource://" + mContext.getPackageName() + "/"); + mVideoView.setVideoURI(uri); + mVideoView.start(); + updateLogText("media is playing"); + } + } + return true; + case KeyEvent.KEYCODE_F: + updateLogText("request StreamEvent"); + mRequestStreamEventFinished = false; + mStreamDataList.clear(); + // TODO: build target URI instead of using channel URI + requestStreamEvent( + mCurrentChannelUri == null ? null : mCurrentChannelUri.toString(), + "event1"); + return true; + case KeyEvent.KEYCODE_G: + updateLogText("change video bounds"); + if (mIsFullScreen) { + setVideoBounds(new Rect(100, 150, 960, 540)); + updateLogText("Change video broadcast size(100, 150, 960, 540)"); + mIsFullScreen = false; + } else { + setVideoBounds(new Rect(0, 0, mScreenWidth, mScreenHeight)); + updateLogText("Change video broadcast full screen"); + mIsFullScreen = true; + } + return true; + case KeyEvent.KEYCODE_H: + updateLogText("request section"); + 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); + } + } + + @Override + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_PROG_RED: + case KeyEvent.KEYCODE_A: + case KeyEvent.KEYCODE_B: + case KeyEvent.KEYCODE_C: + case KeyEvent.KEYCODE_D: + case KeyEvent.KEYCODE_E: + 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); + } + } + + public void updateLogText(String log) { + if (DEBUG) { + Log.d(TAG, log); + } + mLogView.setText(log); + } + + private void updateSurface(Surface surface, int width, int height) { + mHandler.post( + () -> { + // Update our virtualDisplay if it already exists, create a new one otherwise + if (mVirtualDisplay != null) { + mVirtualDisplay.setSurface(surface); + mVirtualDisplay.resize(width, height, DisplayMetrics.DENSITY_DEFAULT); + } else { + DisplayManager displayManager = + mContext.getSystemService(DisplayManager.class); + if (displayManager == null) { + Log.e(TAG, "Failed to get DisplayManager"); + return; + } + mVirtualDisplay = displayManager.createVirtualDisplay(VIRTUAL_DISPLAY_NAME, + width, + height, + DisplayMetrics.DENSITY_DEFAULT, + surface, + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + + Presentation presentation = + new Presentation(mContext, mVirtualDisplay.getDisplay()); + presentation.setContentView(mViewContainer); + presentation.getWindow().setBackgroundDrawable(new ColorDrawable(0)); + presentation.show(); + } + }); + } + + private void initSampleView() { + View sampleView = LayoutInflater.from(mContext).inflate(R.layout.sample_layout, null); + TextView appServiceIdText = sampleView.findViewById(R.id.app_service_id); + appServiceIdText.setText("App Service ID: " + mAppServiceId); + + mTvInputIdView = sampleView.findViewById(R.id.tv_input_id); + mChannelUriView = sampleView.findViewById(R.id.channel_uri); + mVideoTrackView = sampleView.findViewById(R.id.video_track_selected); + mAudioTrackView = sampleView.findViewById(R.id.audio_track_selected); + mSubtitleTrackView = sampleView.findViewById(R.id.subtitle_track_selected); + mLogView = sampleView.findViewById(R.id.log_text); + // Set default values for the selected tracks, since we cannot request data on them directly + mVideoTrackView.setText("No video track selected"); + mAudioTrackView.setText("No audio track selected"); + mSubtitleTrackView.setText("No subtitle track selected"); + + mVideoView = new VideoView(mContext); + mVideoView.setVisibility(View.GONE); + mVideoView.setOnCompletionListener( + new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + mVideoView.setVisibility(View.GONE); + mLogView.setText("MediaPlayer onCompletion"); + tuneChannelByType( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE, + mCurrentTvInputId, + mCurrentChannelUri); + } + }); + mWidth = 0; + mHeight = 0; + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mScreenWidth = wm.getDefaultDisplay().getWidth(); + mScreenHeight = wm.getDefaultDisplay().getHeight(); + + mViewContainer.addView(sampleView); + } + + private void updateTrackSelectedView(int type, String trackId) { + mHandler.post( + () -> { + if (mTracks == null) { + return; + } + TvTrackInfo newSelectedTrack = null; + for (TvTrackInfo track : mTracks) { + if (track.getType() == type && track.getId().equals(trackId)) { + newSelectedTrack = track; + break; + } + } + + if (newSelectedTrack == null) { + if (DEBUG) { + Log.d(TAG, "Did not find selected track within track list"); + } + return; + } + switch (newSelectedTrack.getType()) { + case TvTrackInfo.TYPE_VIDEO: + mVideoTrackView.setText( + "Video Track: id= " + newSelectedTrack.getId() + + ", height=" + newSelectedTrack.getVideoHeight() + + ", width=" + newSelectedTrack.getVideoWidth() + + ", frame_rate=" + newSelectedTrack.getVideoFrameRate() + + ", pixel_ratio=" + newSelectedTrack.getVideoPixelAspectRatio() + ); + break; + case TvTrackInfo.TYPE_AUDIO: + mAudioTrackView.setText( + "Audio Track: id=" + newSelectedTrack.getId() + + ", lang=" + newSelectedTrack.getLanguage() + + ", sample_rate=" + newSelectedTrack.getAudioSampleRate() + + ", channel_count=" + newSelectedTrack.getAudioChannelCount() + ); + break; + case TvTrackInfo.TYPE_SUBTITLE: + mSubtitleTrackView.setText( + "Subtitle Track: id=" + newSelectedTrack.getId() + + ", lang=" + newSelectedTrack.getLanguage() + ); + break; + } + } + ); + } + + 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, + channelUri == null ? null : channelUri.toString()); + parameters.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_INPUT_ID, inputId); + } + mHandler.post(() -> sendPlaybackCommandRequest(type, parameters)); + // Delay request for new information to give time to tune + mHandler.postDelayed( + () -> { + requestCurrentTvInputId(); + requestCurrentChannelUri(); + requestTrackInfoList(); + }, + 1000 + ); + } + + 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); + } + + @Override + public void onCurrentChannelUri(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "onCurrentChannelUri uri=" + channelUri); + } + mCurrentChannelUri = channelUri; + mChannelUriView.setText("Channel URI: " + channelUri); + } + + @Override + public void onTrackInfoList(List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "onTrackInfoList size=" + tracks.size()); + for (int i = 0; i < tracks.size(); i++) { + TvTrackInfo trackInfo = tracks.get(i); + if (trackInfo != null) { + Log.d(TAG, "track " + i + ": type=" + trackInfo.getType() + + " id=" + trackInfo.getId()); + } + } + } + for (TvTrackInfo info : tracks) { + if (info.getType() == TvTrackInfo.TYPE_AUDIO) { + mFirstAudioTrackId = info.getId(); + break; + } + } + mTracks = tracks; + } + + @Override + public void onTracksChanged(List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "onTracksChanged"); + } + onTrackInfoList(tracks); + } + + @Override + public void onTrackSelected(int type, String trackId) { + if (DEBUG) { + 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 + public void onCurrentTvInputId(String inputId) { + if (DEBUG) { + Log.d(TAG, "onCurrentTvInputId id=" + inputId); + } + mCurrentTvInputId = inputId; + mTvInputIdView.setText("TV Input ID: " + inputId); + } + + @Override + public void onTuned(Uri channelUri) { + mCurrentChannelUri = channelUri; + } + + @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) { + handleStreamEventResponse((StreamEventResponse) response); + } else if (mSectionReceived < MAX_HANDLED_RESPONSE + && response instanceof SectionResponse) { + handleSectionResponse((SectionResponse) response); + } else if (response instanceof TableResponse) { + handleTableResponse((TableResponse) response); + } + } + } + + private void handleSectionResponse(SectionResponse response) { + mSectionReceived++; + byte[] data = null; + Bundle params = response.getSessionData(); + if (params != null) { + // TODO: define the key + data = params.getByteArray("key_raw_data"); + } + int version = response.getVersion(); + updateLogText( + "Received section data version = " + + version + + ", data = " + + Arrays.toString(data)); + } + + private void handleStreamEventResponse(StreamEventResponse response) { + updateLogText("Received stream event response"); + byte[] rData = response.getData(); + if (rData == null) { + mRequestStreamEventFinished = true; + updateLogText("Received stream event data is null"); + return; + } + // TODO: convert to Hex instead + String data = Arrays.toString(rData); + if (mStreamDataList.contains(data)) { + return; + } + mStreamDataList.add(data); + updateLogText( + "Received stream event data(" + + (mStreamDataList.size() - 1) + + "): " + + data); + if (mStreamDataList.size() >= MAX_HANDLED_RESPONSE) { + mRequestStreamEventFinished = true; + updateLogText("Received stream event data finished"); + } + } + + 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); + params.putString(TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, trackId); + mHandler.post( + () -> + sendPlaybackCommandRequest( + TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK, + params)); + } + + private int generateRequestId() { + return ++mGeneratedRequestId; + } + + private void requestStreamEvent(String targetUri, String eventName) { + if (targetUri == null) { + return; + } + int requestId = generateRequestId(); + BroadcastInfoRequest request = + new StreamEventRequest( + requestId, + BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE, + Uri.parse(targetUri), + eventName); + requestBroadcastInfo(request); + } + + private void requestSection(boolean repeat, int tsPid, int tableId, int version) { + int requestId = generateRequestId(); + BroadcastInfoRequest request = + new SectionRequest( + requestId, + repeat ? + BroadcastInfoRequest.REQUEST_OPTION_REPEAT : + BroadcastInfoRequest.REQUEST_OPTION_AUTO_UPDATE, + tsPid, + tableId, + 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/lint-baseline.xml b/lint-baseline.xml index d91a1894..29aff212 100644 --- a/lint-baseline.xml +++ b/lint-baseline.xml @@ -25,28 +25,6 @@ <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `updateAndStartServiceIfNeeded`" - errorLine1=" scheduler.updateAndStartServiceIfNeeded();" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/receiver/BootCompletedReceiver.java" - line="90" - column="23"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" if (!TvContract.isChannelUriForPassthroughInput(uri)) {" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/data/ChannelImpl.java" - line="444" - column="25"/> - </issue> - - <issue - id="NewApi" message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#canRecord`" errorLine1=" if (info.canRecord()) {" errorLine2=" ~~~~~~~~~"> @@ -80,17 +58,6 @@ <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence customLabel = input.loadCustomLabel(getContext());" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/ui/InputBannerView.java" - line="75" - column="42"/> - </issue> - - <issue - id="NewApi" message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#canRecord`" errorLine1=" tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;" errorLine2=" ~~~~~~~~~"> @@ -113,193 +80,6 @@ <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" TvContract.isChannelUriForPassthroughInput(getIntent().getData());" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="534" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="1002" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="1029" - column="48"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" TvContract.isChannelUriForPassthroughInput(channelUri)" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="1037" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" if (TvContract.isChannelUriForPassthroughInput(channelUri)) {" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="1065" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" } else if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="1544" - column="35"/> - </issue> - - <issue - id="NewApi" - message="Method reference requires API level 24 (current min is 23): `MainActivity.super::enterPictureInPictureMode`" - errorLine1=" mHandler.post(MainActivity.super::enterPictureInPictureMode);" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="2402" - column="27"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" return TvContract.isChannelUriForPassthroughInput(uri)" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/MainActivity.java" - line="2813" - column="27"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`" - errorLine1=" for (TvContentRating tvContentRating : mTvInputManager.getBlockedRatings()) {" - errorLine2=" ~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java" - line="74" - column="68"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`" - errorLine1=" mRatings = new HashSet<>(mTvInputManager.getBlockedRatings());" - errorLine2=" ~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java" - line="89" - column="50"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`" - errorLine1=" Set<TvContentRating> removed = new HashSet<>(mTvInputManager.getBlockedRatings());" - errorLine2=" ~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java" - line="93" - column="70"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 28 (current min is 23): `android.media.tv.TvInputManager#getBlockedRatings`" - errorLine1=" added.removeAll(mTvInputManager.getBlockedRatings());" - errorLine2=" ~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/parental/ParentalControlSettings.java" - line="100" - column="41"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" if (TvContract.isChannelUriForPassthroughInput(channelUri)) {" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/SelectInputActivity.java" - line="69" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#isHidden`" - errorLine1=" if (!input.isHidden(getContext())) {" - errorLine2=" ~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/ui/SelectInputView.java" - line="253" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence customLabel = input.loadCustomLabel(getContext());" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/ui/SelectInputView.java" - line="287" - column="42"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvView#tune`" - errorLine1=" mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);" - errorLine2=" ~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java" - line="671" - column="21"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#getTunerCount`" - errorLine1=" input.getTunerCount()," - errorLine2=" ~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java" - line="1174" - column="39"/> - </issue> - - <issue - id="NewApi" message="Call requires API level 24 (current min is 23): `createScheduler`" errorLine1=" mRecordingScheduler = RecordingScheduler.createScheduler(this);" errorLine2=" ~~~~~~~~~~~~~~~"> @@ -311,61 +91,6 @@ <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#isHidden`" - errorLine1=" if (!input.isHidden(this)) {" - errorLine2=" ~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/TvApplication.java" - line="402" - column="28"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence inputCustomLabel = info.loadCustomLabel(mContext);" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java" - line="216" - column="62"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence inputCustomLabel = info.loadCustomLabel(mContext);" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java" - line="257" - column="58"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputManager.TvInputCallback#onInputUpdated`" - errorLine1=" callback.onInputUpdated(inputId);" - errorLine2=" ~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java" - line="265" - column="34"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java" - line="279" - column="63"/> - </issue> - - <issue - id="NewApi" message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputManager.TvInputCallback#onTvInputInfoUpdated`" errorLine1=" callback.onTvInputInfoUpdated(inputInfo);" errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -377,46 +102,26 @@ <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" CharSequence customLabelCharSequence = info.loadCustomLabel(mContext);" - errorLine2=" ~~~~~~~~~~~~~~~"> - <location - file="packages/apps/TV/src/com/android/tv/util/TvInputManagerHelper.java" - line="472" - column="57"/> - </issue> - - <issue - id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));" - errorLine2=" ~~~~~~~~~~~~~~~"> + message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#getTunerCount`"> <location - file="packages/apps/TV/src/com/android/tv/search/TvProviderSearch.java" - line="510" - column="58"/> + file="packages/apps/TV/src/com/android/tv/ui/TunableTvView.java" + line="1205"/> </issue> <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvInputInfo#loadCustomLabel`" - errorLine1=" String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));" - errorLine2=" ~~~~~~~~~~~~~~~"> + message="Call requires API level 24 (current min is 23): `updateAndStartServiceIfNeeded`"> <location - file="packages/apps/TV/src/com/android/tv/search/TvProviderSearch.java" - line="535" - column="58"/> + file="packages/apps/TV/src/com/android/tv/receiver/BootCompletedReceiver.java" + line="95"/> </issue> <issue id="NewApi" - message="Call requires API level 24 (current min is 23): `android.media.tv.TvContract#isChannelUriForPassthroughInput`" - errorLine1=" return isChannelUriForTunerInput(uri) || TvContract.isChannelUriForPassthroughInput(uri);" - errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message="Method reference requires API level 24 (current min is 23): `MainActivity.super::enterPictureInPictureMode`"> <location - file="packages/apps/TV/src/com/android/tv/util/Utils.java" - line="276" - column="61"/> + file="packages/apps/TV/src/com/android/tv/MainActivity.java" + line="2435"/> </issue> -</issues> +</issues>
\ No newline at end of file diff --git a/res/drawable/tv_iapp_dialog_background.xml b/res/drawable/tv_iapp_dialog_background.xml new file mode 100755 index 00000000..3f6f8e6c --- /dev/null +++ b/res/drawable/tv_iapp_dialog_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/tv_iapp_dialog_background"/> + <corners android:radius="2dp" /> +</shape> diff --git a/res/layout/activity_tv.xml b/res/layout/activity_tv.xml index b6a0a3a3..6347f897 100644 --- a/res/layout/activity_tv.xml +++ b/res/layout/activity_tv.xml @@ -28,6 +28,12 @@ android:layout_height="match_parent" android:layout_gravity="start|center_vertical" /> + <android.media.tv.interactive.TvInteractiveAppView + android:id="@+id/tv_app_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/transparent" /> + <FrameLayout android:id="@+id/scene_container" android:layout_height="match_parent" diff --git a/res/layout/tv_app_dialog.xml b/res/layout/tv_app_dialog.xml new file mode 100755 index 00000000..e12e0bf7 --- /dev/null +++ b/res/layout/tv_app_dialog.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/pin_dialog_width" + android:layout_height="wrap_content" + android:paddingTop="19dp" + android:paddingBottom="24dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:elevation="8dp" + android:background="@drawable/tv_iapp_dialog_background"> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView + android:id="@+id/title" + android:layout_width="@dimen/pin_dialog_title_width" + android:layout_height="wrap_content" + android:layout_marginBottom="7dp" + android:layout_centerHorizontal="true" + android:lineSpacingExtra="@dimen/pin_dialog_text_line_spacing" + android:textSize="@dimen/pin_dialog_text_size" + android:textColor="@color/tv_iapp_dialog_text_color" + android:fontFamily="@string/font" + android:singleLine="false" /> + <LinearLayout + android:layout_below="@id/title" + android:layout_marginTop="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:orientation="horizontal" + > + <Button + android:id="@+id/ok" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:gravity="center" + android:text="ok" + android:importantForAccessibility="yes" + android:paddingStart="24dp" + android:paddingEnd="24dp" /> + <Button + android:id="@+id/cancel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginLeft="30dp" + android:gravity="center" + android:text="cancel" + android:importantForAccessibility="yes" + android:paddingStart="24dp" + android:paddingEnd="24dp" /> + </LinearLayout> + </RelativeLayout> +</FrameLayout> diff --git a/res/values/arrays-custom.xml b/res/values/arrays-custom.xml index 252d6f4f..10f4402d 100644 --- a/res/values/arrays-custom.xml +++ b/res/values/arrays-custom.xml @@ -42,4 +42,17 @@ <item>Set up your newly installed channel sources to customize your channel list. \nChoose the Channel sources within the Settings menu to get started.</item> </string-array> + + <!-- An array of input setup component names in the form of + <code>input_id + '#' + flattened_component_name</code>. + If one input's setup component is defined by this runtime resource overlay (RRO), + the LiveTv will use the defined component to set up the input, + instead of the setup Activity defined in the TvInputService apk.--> + <string-array translatable="false" name="setup_ComponentNames"> + <!-- Example: + <item>"input_1#com.example.setup1/.SetupActivity1"</item> + <item>"input_2#com.example.setup1/com.example.setup1.SetupActivity2"</item> + <item>"input_3#com.example.setup2/com.example2.SetupActivity"</item> + --> + </string-array> </resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index b68feb13..f46d7b9b 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -160,4 +160,8 @@ <color name="dvr_detail_default_background_scrim">#CC000000</color> <color name="dvr_recording_failed_text_color">#FFCDD2</color> <color name="dvr_recording_conflict_text_color">#FFE082</color> + + <!-- TV IAPP dialog --> + <color name="tv_iapp_dialog_background">#384248</color> + <color name="tv_iapp_dialog_text_color">#C0EEEEEE</color> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index e272244d..b36827d1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1044,4 +1044,12 @@ Selecting \"Allow\", enables <xliff:g id="app_name">Live TV</xliff:g> to immediately free storage space when deleting recorded TV programs. This makes more space available for new recordings.</string> + + <!-- Interactive Application Dialog--> + <string name="tv_app_dialog_title">An interactive app was found. Do you want to turn on interactive apps?</string> + + <!-- Interactive Application Setting --> + <string name="interactive_app_settings">Interactive app settings</string> + <string name="tv_iapp_on">On</string> + <string name="tv_iapp_off">Off</string> </resources> diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index fe138980..351f0010 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -36,7 +36,7 @@ import java.util.Map; import java.util.Set; /** - * It manages the current tuned channel among browsable channels. And it determines the next channel + * Manages the current tuned channel among browsable channels, and determines the next channel * by channel up/down. But, it doesn't actually tune through TvView. */ @MainThread diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index ea17751b..57fc883e 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -18,6 +18,7 @@ package com.android.tv; import android.annotation.TargetApi; import android.content.Context; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvTrackInfo; @@ -582,6 +583,12 @@ public class InputSessionManager { public void onSignalStrength(String inputId, int value) { mDelegate.onSignalStrength(inputId, value); } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + mDelegate.onAitInfoUpdated(inputId, aitInfo); + } } /** Called when the {@link TvView} channel is changed. */ diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index 8dbafe47..cea293de 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -18,6 +18,7 @@ package com.android.tv; import static com.android.tv.common.feature.SystemAppFeature.SYSTEM_APP_FEATURE; +import android.annotation.TargetApi; import android.app.Activity; import android.app.PendingIntent; import android.app.SearchManager; @@ -32,6 +33,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.hardware.display.DisplayManager; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; @@ -40,6 +42,8 @@ import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.media.tv.TvTrackInfo; import android.media.tv.TvView.OnUnhandledInputEventListener; +import android.media.tv.interactive.TvInteractiveAppManager; +import android.media.tv.interactive.TvInteractiveAppView; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -105,6 +109,8 @@ import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment.OnInteractiveAppCheckedListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.recorder.ConflictChecker; @@ -115,6 +121,7 @@ import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.features.TvFeatures; import com.android.tv.guide.ProgramItemView; +import com.android.tv.interactive.IAppManager; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; @@ -193,7 +200,8 @@ public class MainActivity extends Activity OnPinCheckedListener, ChannelChanger, HasSingletons<MySingletons>, - HasAndroidInjector { + HasAndroidInjector, + OnInteractiveAppCheckedListener { private static final String TAG = "MainActivity"; private static final boolean DEBUG = false; private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; @@ -254,6 +262,9 @@ public class MainActivity extends Activity 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; @@ -365,6 +376,8 @@ public class MainActivity extends Activity private String mLastInputIdFromIntent; + private IAppManager mIAppManager; + private final Handler mHandler = new MainActivityHandler(this); private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>(); @@ -406,6 +419,13 @@ public class MainActivity extends Activity tune(true); } break; + case TvInteractiveAppManager.ACTION_APP_LINK_COMMAND: + if (DEBUG) { + Log.d(TAG, "Received action link command"); + } + // TODO: handle the command + break; + default: // fall out } } @@ -545,8 +565,10 @@ public class MainActivity extends Activity return; } setContentView(R.layout.activity_tv); + TvInteractiveAppView tvInteractiveAppView = findViewById(R.id.tv_app_view); mTvView = findViewById(R.id.main_tunable_tv_view); - mTvView.initialize(mProgramDataManager, mTvInputManagerHelper, mLegacyFlags); + mTvView.initialize( + mProgramDataManager, mTvInputManagerHelper, mLegacyFlags, tvInteractiveAppView); mTvView.setOnUnhandledInputEventListener( new OnUnhandledInputEventListener() { @Override @@ -717,8 +739,8 @@ public class MainActivity extends Activity mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null); mAudioCapabilitiesReceiver.register(); Intent nowPlayingIntent = new Intent(this, MainActivity.class); - PendingIntent pendingIntent = - PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, + nowPlayingIntent, PendingIntent.FLAG_IMMUTABLE); mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent); mTvViewUiManager.restoreDisplayMode(false); @@ -732,9 +754,21 @@ public class MainActivity extends Activity mDvrConflictChecker = new ConflictChecker(this); } initForTest(); + if (TvFeatures.HAS_TIAF.isEnabled(this)) { + mIAppManager = new IAppManager(this, mTvView, mHandler); + } Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end"); } + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onInteractiveAppChecked(boolean checked) { + TvSettings.setTvIAppOn(getApplicationContext(), checked); + if (checked) { + mIAppManager.processHeldAitInfo(); + } + } + private void startOnboardingActivity() { startActivity(OnboardingActivity.buildIntent(this, getIntent())); finish(); @@ -833,7 +867,7 @@ public class MainActivity extends Activity mMainDurationTimer.start(); applyParentalControlSettings(); - registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER); + registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER, Context.RECEIVER_EXPORTED); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Intent notificationIntent = new Intent(this, NotificationService.class); @@ -1081,7 +1115,7 @@ public class MainActivity extends Activity } mTvView.start(); - mAudioManagerHelper.setVolumeByAudioFocusStatus(); + mAudioManagerHelper.requestAudioFocus(); tune(true); } @@ -1126,6 +1160,9 @@ public class MainActivity extends Activity private void stopAll(boolean keepVisibleBehind) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); stopTv("stopAll()", keepVisibleBehind); + if (mIAppManager != null) { + mIAppManager.stop(); + } } public TvInputManagerHelper getTvInputManagerHelper() { @@ -1138,7 +1175,7 @@ public class MainActivity extends Activity * @param calledByPopup If true, startSetupActivity is invoked from the setup fragment. */ public void startSetupActivity(TvInputInfo input, boolean calledByPopup) { - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show(); return; @@ -1425,6 +1462,9 @@ public class MainActivity extends Activity if (DeveloperPreferences.LOG_KEYEVENT.get(this)) { Log.d(TAG, "dispatchKeyEvent(" + event + ")"); } + if (mIAppManager != null && mIAppManager.dispatchKeyEvent(event)) { + return true; + } // If an activity is closed on a back key down event, back key down events with none zero // repeat count or a back key up event can be happened without the first back key down // event which should be ignored in this activity. @@ -1631,7 +1671,7 @@ public class MainActivity extends Activity } } - private void stopTv() { + public void stopTv() { stopTv(null, false); } @@ -1932,12 +1972,21 @@ public class MainActivity extends Activity @VisibleForTesting protected void applyMultiAudio(String trackId) { + applyMultiAudio(false, trackId); + } + + @VisibleForTesting + protected void applyMultiAudio(boolean allowAutoSelection, String trackId) { + if (!allowAutoSelection && trackId == null) { + selectTrack(TvTrackInfo.TYPE_AUDIO, null, UNDEFINED_TRACK_INDEX); + mTvOptionsManager.onMultiAudioChanged(null); + return; + } List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO); if (tracks == null) { mTvOptionsManager.onMultiAudioChanged(null); return; } - TvTrackInfo bestTrack = null; if (trackId != null) { for (TvTrackInfo track : tracks) { @@ -2459,7 +2508,7 @@ public class MainActivity extends Activity return handled; } - private boolean isKeyEventBlocked() { + public boolean isKeyEventBlocked() { // If the current channel is a passthrough channel, we don't handle the key events in TV // activity. Instead, the key event will be handled by the passthrough TV input. return mChannelTuner.isCurrentChannelPassthrough(); @@ -2907,7 +2956,7 @@ public class MainActivity extends Activity } applyDisplayRefreshRate(info.getVideoFrameRate()); mTvViewUiManager.updateTvAspectRatio(); - applyMultiAudio( + applyMultiAudio(allowAutoSelectionOfTrack, allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO)); applyClosedCaption(); mOverlayManager.getMenu().onStreamInfoChanged(); @@ -2989,6 +3038,14 @@ public class MainActivity extends Activity TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH); } } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + if (mIAppManager != null) { + mIAppManager.onAitInfoUpdated(aitInfo); + } + } } private class MySingletonsImpl implements MySingletons { @@ -3047,5 +3104,8 @@ public class MainActivity extends Activity @ContributesAndroidInjector abstract DvrScheduleFragment contributesDvrScheduleFragment(); + + @ContributesAndroidInjector + abstract InteractiveAppDialogFragment contributesInteractiveAppDialogFragment(); } } diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index e7f89108..2a4a556f 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -109,7 +109,6 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } - SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during // setupIntent.putExtras(intent.getExtras()). @@ -127,6 +126,7 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } + SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); } catch (ActivityNotFoundException e) { Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java index 5d0e9c82..59e2406f 100644 --- a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java +++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java @@ -15,11 +15,14 @@ */ package com.android.tv.audiotvservice; +import android.annotation.TargetApi; import android.app.Notification; import android.app.Service; import android.content.Intent; import android.media.session.MediaSession; +import android.media.tv.AitInfo; import android.net.Uri; +import android.os.Build; import android.os.IBinder; import android.support.annotation.Nullable; import android.util.Log; @@ -99,4 +102,8 @@ public class AudioOnlyTvService extends Service implements OnTuneListener { @Override public void onChannelSignalStrength() {} + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {} } diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java index f31290d0..5be1179d 100644 --- a/src/com/android/tv/data/ChannelImpl.java +++ b/src/com/android/tv/data/ChannelImpl.java @@ -18,6 +18,7 @@ package com.android.tv.data; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.media.tv.TvContract; @@ -673,7 +674,18 @@ public final class ChannelImpl implements Channel { if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) { try { Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); - if (intent.resolveActivityInfo(pm, 0) != null) { + ActivityInfo activityInfo = intent.resolveActivityInfo(pm, 0); + if (activityInfo != null) { + String packageName = activityInfo.packageName; + // Prevent creation of App Links to private activities in this package + boolean isProtectedActivity = packageName != null + && (packageName.equals(CommonConstants.BASE_PACKAGE) + || packageName.startsWith(CommonConstants.BASE_PACKAGE + ".")); + if (isProtectedActivity) { + Log.w(TAG,"Attempt to add app link to protected activity: " + + mAppLinkIntentUri); + return; + } mAppLinkIntent = intent; mAppLinkIntent.putExtra( CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index e4237bf4..f323423c 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -44,6 +44,8 @@ public interface StreamInfo { int getAudioChannelCount(); + float getStreamVolume(); + boolean hasClosedCaption(); boolean isVideoAvailable(); diff --git a/src/com/android/tv/dialog/InteractiveAppDialogFragment.java b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java new file mode 100755 index 00000000..c5ffbaac --- /dev/null +++ b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dialog; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.TextView; +import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; + +import java.util.function.Function; + +import dagger.android.AndroidInjection; + +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class InteractiveAppDialogFragment extends SafeDismissDialogFragment { + private static final boolean DEBUG = false; + + public static final String DIALOG_TAG = InteractiveAppDialogFragment.class.getName(); + private static final String TRACKER_LABEL = "Interactive App Dialog"; + private static final String TV_IAPP_NAME = "tv_iapp_name"; + private boolean mIsChoseOK; + private String mIAppName; + private Function mUpdateAitInfo; + + public static InteractiveAppDialogFragment create(String iappName) { + InteractiveAppDialogFragment fragment = new InteractiveAppDialogFragment(); + Bundle args = new Bundle(); + args.putString(TV_IAPP_NAME, iappName); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(Context context) { + AndroidInjection.inject(this); + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIAppName = getArguments().getString(TV_IAPP_NAME); + setStyle(STYLE_NO_TITLE, 0); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dlg = super.onCreateDialog(savedInstanceState); + dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; + mIsChoseOK = false; + return dlg; + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + @Override + public void onStart() { + super.onStart(); + // Dialog size is determined by its windows size, not inflated view size. + // So apply view size to window after the DialogFragment.onStart() where dialog is shown. + Dialog dlg = getDialog(); + if (dlg != null) { + dlg.getWindow() + .setLayout( + getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), + LayoutParams.WRAP_CONTENT); + } + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View v = inflater.inflate(R.layout.tv_app_dialog, container, false); + TextView mTitleView = (TextView) v.findViewById(R.id.title); + mTitleView.setText(getString(R.string.tv_app_dialog_title, mIAppName)); + Button okButton = v.findViewById(R.id.ok); + okButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exit(true); + } + }); + Button cancelButton = v.findViewById(R.id.cancel); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exit(false); + } + }); + return v; + } + + private void exit(boolean isokclick) { + mIsChoseOK = isokclick; + dismiss(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + SoftPreconditions.checkState(getActivity() instanceof OnInteractiveAppCheckedListener); + if (getActivity() instanceof OnInteractiveAppCheckedListener) { + ((OnInteractiveAppCheckedListener) getActivity()) + .onInteractiveAppChecked(mIsChoseOK); + } + } + + public interface OnInteractiveAppCheckedListener { + void onInteractiveAppChecked(boolean checked); + } +} diff --git a/src/com/android/tv/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java index f309537d..475c17f8 100644 --- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java @@ -322,7 +322,8 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); - PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + PendingIntent alarmIntent = + PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE); // This will cancel the previous alarm. mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); } else { diff --git a/src/com/android/tv/features/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java index 5282c28c..ebd7cb9a 100644 --- a/src/com/android/tv/features/TvFeatures.java +++ b/src/com/android/tv/features/TvFeatures.java @@ -101,5 +101,8 @@ public final class TvFeatures extends CommonFeatures { /** Use input blocklist to disable partner's tuner input. */ public static final Feature USE_PARTNER_INPUT_BLOCKLIST = ON; + /** Support for interactive applications using the TIAF **/ + public static final Feature HAS_TIAF = Sdk.AT_LEAST_T; + private TvFeatures() {} } diff --git a/src/com/android/tv/interactive/IAppManager.java b/src/com/android/tv/interactive/IAppManager.java new file mode 100644 index 00000000..682b35c6 --- /dev/null +++ b/src/com/android/tv/interactive/IAppManager.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.interactive; + +import static com.android.tv.util.CaptionSettings.OPTION_OFF; +import static com.android.tv.util.CaptionSettings.OPTION_ON; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.media.tv.TvTrackInfo; +import android.media.tv.interactive.TvInteractiveAppManager; +import android.media.tv.AitInfo; +import android.media.tv.interactive.TvInteractiveAppService; +import android.media.tv.interactive.TvInteractiveAppServiceInfo; +import android.media.tv.interactive.TvInteractiveAppView; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.ContentUriUtils; +import com.android.tv.data.api.Channel; +import com.android.tv.dialog.InteractiveAppDialogFragment; +import com.android.tv.features.TvFeatures; +import com.android.tv.ui.TunableTvView; +import com.android.tv.util.TvSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class IAppManager { + private static final String TAG = "IAppManager"; + private static final boolean DEBUG = false; + + private final MainActivity mMainActivity; + private final TvInteractiveAppManager mTvIAppManager; + private final TvInteractiveAppView mTvIAppView; + private final TunableTvView mTvView; + private final Handler mHandler; + private AitInfo mCurrentAitInfo; + private AitInfo mHeldAitInfo; // AIT info that has been held pending dialog confirmation + private boolean mTvAppDialogShown = false; + + public IAppManager(@NonNull MainActivity parentActivity, @NonNull TunableTvView tvView, + @NonNull Handler handler) { + SoftPreconditions.checkFeatureEnabled(parentActivity, TvFeatures.HAS_TIAF, TAG); + + mMainActivity = parentActivity; + mTvView = tvView; + mHandler = handler; + mTvIAppManager = mMainActivity.getSystemService(TvInteractiveAppManager.class); + mTvIAppView = mMainActivity.findViewById(R.id.tv_app_view); + if (mTvIAppManager == null || mTvIAppView == null) { + Log.e(TAG, "Could not find interactive app view or manager"); + return; + } + + ExecutorService executor = Executors.newSingleThreadExecutor(); + mTvIAppManager.registerCallback( + executor, + new MyInteractiveAppManagerCallback() + ); + mTvIAppView.setCallback( + executor, + new MyInteractiveAppViewCallback() + ); + mTvIAppView.setOnUnhandledInputEventListener(executor, + inputEvent -> { + if (mMainActivity.isKeyEventBlocked()) { + return true; + } + if (inputEvent instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) inputEvent; + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN + && keyEvent.isLongPress()) { + if (mMainActivity.onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { + return true; + } + } + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + return mMainActivity.onKeyUp(keyEvent.getKeyCode(), keyEvent); + } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + return mMainActivity.onKeyDown(keyEvent.getKeyCode(), keyEvent); + } + } + return false; + }); + } + + public void stop() { + mTvIAppView.stopInteractiveApp(); + mTvIAppView.reset(); + mCurrentAitInfo = null; + } + + /* + * Update current info based on ait info that was held when the dialog was shown. + */ + public void processHeldAitInfo() { + if (mHeldAitInfo != null) { + onAitInfoUpdated(mHeldAitInfo); + } + } + + public boolean dispatchKeyEvent(KeyEvent event) { + if (mTvIAppView != null && mTvIAppView.getVisibility() == View.VISIBLE + && mTvIAppView.dispatchKeyEvent(event)){ + return true; + } + return false; + } + + public void onAitInfoUpdated(AitInfo aitInfo) { + if (mTvIAppManager == null || aitInfo == null) { + return; + } + if (mCurrentAitInfo != null && mCurrentAitInfo.getType() == aitInfo.getType()) { + if (DEBUG) { + Log.d(TAG, "Ignoring AIT update: Same type as current"); + } + return; + } + + List<TvInteractiveAppServiceInfo> tvIAppInfoList = + mTvIAppManager.getTvInteractiveAppServiceList(); + if (tvIAppInfoList.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Ignoring AIT update: No interactive app services registered"); + } + return; + } + + // App Type ID numbers allocated by DVB Services + int type = -1; + switch (aitInfo.getType()) { + case 0x0010: // HBBTV + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_HBBTV; + break; + case 0x0006: // DCAP-J: DCAP Java applications + case 0x0007: // DCAP-X: DCAP XHTML applications + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_ATSC; + break; + case 0x0001: // Ginga-J + case 0x0009: // Ginga-NCL + case 0x000b: // Ginga-HTML5 + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_GINGA; + break; + default: + Log.e(TAG, "AIT info contained unknown type: " + aitInfo.getType()); + return; + } + + if (TvSettings.isTvIAppOn(mMainActivity.getApplicationContext())) { + mTvAppDialogShown = false; + for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { + if ((info.getSupportedTypes() & type) > 0) { + mCurrentAitInfo = aitInfo; + if (mTvIAppView != null) { + mTvIAppView.setVisibility(View.VISIBLE); + mTvIAppView.prepareInteractiveApp(info.getId(), type); + } + break; + } + } + } else if (!mTvAppDialogShown) { + if (DEBUG) { + Log.d(TAG, "TV IApp is not enabled"); + } + + for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { + if ((info.getSupportedTypes() & type) > 0) { + mMainActivity.getOverlayManager().showDialogFragment( + InteractiveAppDialogFragment.DIALOG_TAG, + InteractiveAppDialogFragment.create(info.getServiceInfo().packageName), + false); + mHeldAitInfo = aitInfo; + mTvAppDialogShown = true; + break; + } + } + } + } + + private class MyInteractiveAppManagerCallback extends + TvInteractiveAppManager.TvInteractiveAppCallback { + @Override + public void onInteractiveAppServiceAdded(String iAppServiceId) {} + + @Override + public void onInteractiveAppServiceRemoved(String iAppServiceId) {} + + @Override + public void onInteractiveAppServiceUpdated(String iAppServiceId) {} + + @Override + public void onTvInteractiveAppServiceStateChanged(String iAppServiceId, int type, int state, + int err) { + if (state == TvInteractiveAppManager.SERVICE_STATE_READY && mTvIAppView != null) { + mTvIAppView.startInteractiveApp(); + mTvIAppView.setTvView(mTvView.getTvView()); + if (mTvView.getTvView() != null) { + mTvView.getTvView().setInteractiveAppNotificationEnabled(true); + } + } + } + } + + private class MyInteractiveAppViewCallback extends + TvInteractiveAppView.TvInteractiveAppCallback { + @Override + public void onPlaybackCommandRequest(String iAppServiceId, String cmdType, + Bundle parameters) { + if (mTvView == null || cmdType == null) { + return; + } + switch (cmdType) { + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE: + if (parameters == null) { + return; + } + String uriString = parameters.getString( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI); + if (uriString != null) { + Uri channelUri = Uri.parse(uriString); + Channel channel = mMainActivity.getChannelDataManager().getChannel( + ContentUriUtils.safeParseId(channelUri)); + if (channel != null) { + mHandler.post(() -> mMainActivity.tuneToChannel(channel)); + } + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK: + if (mTvView != null && parameters != null) { + int trackType = parameters.getInt( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, + -1); + String trackId = parameters.getString( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, + null); + switch (trackType) { + case TvTrackInfo.TYPE_AUDIO: + // When trackId is null, deselects current audio track. + mHandler.post(() -> mMainActivity.selectAudioTrack(trackId)); + break; + case TvTrackInfo.TYPE_SUBTITLE: + // When trackId is null, turns off captions. + mHandler.post(() -> mMainActivity.selectSubtitleTrack( + trackId == null ? OPTION_OFF : OPTION_ON, trackId)); + break; + } + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME: + if (parameters == null) { + return; + } + float volume = parameters.getFloat( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_VOLUME, -1); + if (volume >= 0.0 && volume <= 1.0) { + mHandler.post(() -> mTvView.setStreamVolume(volume)); + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT: + mHandler.post(mMainActivity::channelUp); + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_PREV: + 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: + Log.e(TAG, "PlaybackCommandRequest had unknown cmdType:" + + cmdType); + break; + } + } + + @Override + public void onStateChanged(String iAppServiceId, int state, int err) { + } + + @Override + public void onBiInteractiveAppCreated(String iAppServiceId, Uri biIAppUri, + String biIAppId) {} + + @Override + public void onTeletextAppStateChanged(String iAppServiceId, int state) {} + + @Override + public void onSetVideoBounds(String iAppServiceId, Rect rect) { + if (mTvView != null) { + ViewGroup.MarginLayoutParams layoutParams = mTvView.getTvViewLayoutParams(); + layoutParams.setMargins(rect.left, rect.top, rect.right, rect.bottom); + mTvView.setTvViewLayoutParams(layoutParams); + } + } + + @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; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + Uri currentUri = (currentChannel == null) + ? null + : currentChannel.getUri(); + mTvIAppView.sendCurrentChannelUri(currentUri); + } + + @Override + public void onRequestCurrentChannelLcn(String iAppServiceId) { + if (mTvIAppView == null) { + return; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + if (currentChannel == null || currentChannel.getDisplayNumber() == null) { + return; + } + // Expected format is major channel number, delimiter, minor channel number + String displayNumber = currentChannel.getDisplayNumber(); + String format = "[0-9]+" + Channel.CHANNEL_NUMBER_DELIMITER + "[0-9]+"; + if (!displayNumber.matches(format)) { + return; + } + // Major channel number is returned + String[] numbers = displayNumber.split( + String.valueOf(Channel.CHANNEL_NUMBER_DELIMITER)); + mTvIAppView.sendCurrentChannelLcn(Integer.parseInt(numbers[0])); + } + + @Override + public void onRequestStreamVolume(String iAppServiceId) { + if (mTvIAppView == null || mTvView == null) { + return; + } + mTvIAppView.sendStreamVolume(mTvView.getStreamVolume()); + } + + @Override + public void onRequestTrackInfoList(String iAppServiceId) { + if (mTvIAppView == null || mTvView == null) { + return; + } + List<TvTrackInfo> allTracks = new ArrayList<>(); + int[] trackTypes = new int[] {TvTrackInfo.TYPE_AUDIO, + TvTrackInfo.TYPE_VIDEO, TvTrackInfo.TYPE_SUBTITLE}; + + for (int trackType : trackTypes) { + List<TvTrackInfo> currentTracks = mTvView.getTracks(trackType); + if (currentTracks == null) { + continue; + } + for (TvTrackInfo track : currentTracks) { + if (track != null) { + allTracks.add(track); + } + } + } + mTvIAppView.sendTrackInfoList(allTracks); + } + + @Override + public void onRequestCurrentTvInputId(String iAppServiceId) { + if (mTvIAppView == null) { + return; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + String currentInputId = (currentChannel == null) + ? null + : currentChannel.getInputId(); + mTvIAppView.sendCurrentTvInputId(currentInputId); + } + + @Override + public void onRequestSigning(String iAppServiceId, String signingId, String algorithm, + String alias, byte[] data) {} + } +} diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index dd386d81..0ce5d931 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -193,7 +193,7 @@ public class OnboardingActivity extends SetupActivity { params.getString( SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText( this, diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java index 5fa7606d..9578e243 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -67,7 +67,8 @@ public final class AudioCapabilitiesReceiver { } public void register() { - mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG), + Context.RECEIVER_EXPORTED); } public void unregister() { diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 0eb03bec..0bf6ecf3 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -56,6 +56,11 @@ public class BootCompletedReceiver extends BroadcastReceiver { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); return; } + String action = intent.getAction(); + if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) { + Log.w(TAG, "invalid action " + action); + return; + } if (DEBUG) Log.d(TAG, "boot completed " + intent); Starter.start(context); diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java index 7bf04692..b39ac4ea 100644 --- a/src/com/android/tv/setup/SystemSetupActivity.java +++ b/src/com/android/tv/setup/SystemSetupActivity.java @@ -53,6 +53,7 @@ public class SystemSetupActivity extends SetupActivity { private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; @Inject TvInputManagerHelper mInputManager; + @Inject SetupUtils mSetupUtils; @Inject UiFlags mUiFlags; @Override @@ -97,7 +98,7 @@ public class SystemSetupActivity extends SetupActivity { params.getString( SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText( this, diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index a0cfad32..8265d178 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -287,10 +287,18 @@ public class SelectInputView extends VerticalGridView CharSequence customLabel = input.loadCustomLabel(getContext()); CharSequence label = input.loadLabel(getContext()); if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) { - inputLabelView.setText(label); + if (input.isPassthroughInput()) { + inputLabelView.setText(label); + } else { + inputLabelView.setText(R.string.input_long_label_for_tuner); + } secondaryInputLabelView.setVisibility(View.GONE); } else { - inputLabelView.setText(customLabel); + if (input.isPassthroughInput()) { + inputLabelView.setText(customLabel); + } else { + inputLabelView.setText(R.string.input_long_label_for_tuner); + } secondaryInputLabelView.setText(label); secondaryInputLabelView.setVisibility(View.VISIBLE); } diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index a736e79d..3ac841c2 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -19,6 +19,7 @@ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; @@ -28,12 +29,14 @@ import android.graphics.PorterDuff; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.PlaybackParams; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.TvView.OnUnhandledInputEventListener; +import android.media.tv.interactive.TvInteractiveAppView; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; @@ -194,6 +197,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV private final InputSessionManager mInputSessionManager; private int mChannelSignalStrength; + private TvInteractiveAppView mTvIAppView; private final TvInputCallbackCompat mCallback = new TvInputCallbackCompat() { @@ -413,6 +417,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV mOnTuneListener.onChannelSignalStrength(); } } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + if (!TvFeatures.HAS_TIAF.isEnabled(getContext())) { + return; + } + if (DEBUG) { + Log.d(TAG, + "onAitInfoUpdated: {inputId=" + + inputId + + ", AitInfo=(" + + aitInfo + +")}"); + } + if (mOnTuneListener != null) { + mOnTuneListener.onAitInfoUpdated(inputId, aitInfo); + } + } }; public TunableTvView(Context context) { @@ -476,18 +499,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV }); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); } + public void initialize( + ProgramDataManager programDataManager, + TvInputManagerHelper tvInputManagerHelper, + LegacyFlags legacyFlags) { + initialize(programDataManager, tvInputManagerHelper, legacyFlags, null); + } public void initialize( ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper, - LegacyFlags mLegacyFlags) { + LegacyFlags legacyFlags, + TvInteractiveAppView tvIAppView) { mTvView = findViewById(R.id.tv_view); - mTvView.setUseSecureSurface(!BuildConfig.ENG && !mLegacyFlags.enableDeveloperFeatures()); + mTvView.setUseSecureSurface(!BuildConfig.ENG && !legacyFlags.enableDeveloperFeatures()); mProgramDataManager = programDataManager; mInputManagerHelper = tvInputManagerHelper; mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings(); + mTvIAppView = tvIAppView; if (mInputSessionManager != null) { mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback); } else { @@ -715,6 +746,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } } + @Override + public float getStreamVolume() { + return mIsMuted + ? 0 + : mVolume; + } + /** * Sets fixed size for the internal {@link android.view.Surface} of {@link * android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the @@ -773,6 +811,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV void onContentAllowed(); void onChannelSignalStrength(); + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + void onAitInfoUpdated(String inputId, AitInfo aitInfo); } public void unblockContent(TvContentRating rating) { @@ -976,6 +1017,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV return; } mBlockScreenView.setVisibility(VISIBLE); + if (mTvIAppView != null) { + mTvIAppView.setVisibility(INVISIBLE); + } mBlockScreenView.setBackgroundImage(null); if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) { mBlockScreenView.setIconVisibility(true); @@ -1007,6 +1051,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV if (mBlockScreenView.getVisibility() == VISIBLE) { mBlockScreenView.fadeOut(); } + if (mTvIAppView != null) { + mTvIAppView.setVisibility(VISIBLE); + } } } diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index cf1a9113..19af23b9 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -55,6 +55,7 @@ import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; import com.android.tv.guide.ProgramGuide; @@ -198,6 +199,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { AVAILABLE_DIALOG_TAGS.add(LicenseDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG); + AVAILABLE_DIALOG_TAGS.add(InteractiveAppDialogFragment.DIALOG_TAG); } private final MainActivity mMainActivity; diff --git a/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java new file mode 100755 index 00000000..b56a1d66 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.ui.sidepanel; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; +import java.util.ArrayList; +import java.util.List; + +public class InteractiveAppSettingsFragment extends SideFragment { + private static final String TRACKER_LABEL = "Interactive Application Settings"; + @Override + protected String getTitle() { + return getString(R.string.interactive_app_settings); + } + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + @Override + protected List<Item> getItemList() { + List<Item> items = new ArrayList<>(); + items.add( + new SwitchItem( + getString(R.string.tv_iapp_on), + getString(R.string.tv_iapp_off)) { + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked(TvSettings.isTvIAppOn(getContext())); + } + @Override + protected void onSelected() { + super.onSelected(); + boolean checked = isChecked(); + TvSettings.setTvIAppOn(getContext(), checked); + } + }); + return items; + } +} diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index 1c03b6a9..762a190c 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -29,6 +29,7 @@ import com.android.tv.common.CommonPreferences; import com.android.tv.common.customization.CustomizationManager; import com.android.tv.common.util.PermissionUtils; import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.features.TvFeatures; import com.android.tv.license.LicenseSideFragment; import com.android.tv.license.Licenses; import com.android.tv.util.Utils; @@ -190,6 +191,22 @@ public class SettingsFragment extends SideFragment { } }); } + + //Interactive Application Settings + if (TvFeatures.HAS_TIAF.isEnabled(getContext())) + { + items.add( + new ActionItem(getString(R.string.interactive_app_settings)) { + @Override + protected void onSelected() { + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() + .show(new InteractiveAppSettingsFragment(), false); + } + }); + } + // Show version. SimpleActionItem version = new SimpleActionItem( diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 52b3e3e8..aaee1047 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -31,14 +31,18 @@ import android.support.annotation.UiThread; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.dagger.annotations.ApplicationContext; import com.android.tv.common.singletons.HasTvInputId; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.api.Channel; import com.android.tv.tunerinputcontroller.BuiltInTunerManager; import com.google.common.base.Optional; + +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -362,6 +366,52 @@ public class SetupUtils { } /** + * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined + * in the overlayable resources precedes the one defined in the corresponding TV input service. + */ + @Nullable + public Intent createSetupIntent(Context context, TvInputInfo input) { + String[] componentStrings = context.getResources() + .getStringArray(R.array.setup_ComponentNames); + + if (componentStrings != null) { + for (String component : componentStrings) { + String[] split = component.split("#"); + if (split.length != 2) { + Log.w(TAG, "Invalid component item: " + Arrays.toString(split)); + continue; + } + + final String inputId = split[0].trim(); + if (inputId.equals(input.getId())) { + final String flattenedComponentName = split[1].trim(); + final ComponentName componentName = ComponentName + .unflattenFromString(flattenedComponentName); + if (componentName == null) { + Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName); + continue; + } + + final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN); + overlaySetupIntent.setComponent(componentName); + overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId); + + PackageManager pm = context.getPackageManager(); + if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) { + Log.w(TAG, "unable to find component" + flattenedComponentName); + continue; + } + + Log.i(TAG, "overlay input id: " + inputId + + " to setup activity: " + flattenedComponentName); + return CommonUtils.createSetupIntent(overlaySetupIntent, inputId); + } + } + } + return CommonUtils.createSetupIntent(input); + } + + /** * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} * for {@code inputId}. */ diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index ae79e7e5..1a5434cb 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -53,6 +53,9 @@ public final class TvSettings { private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level"; private static final String PREF_DISABLE_PIN_UNTIL = "pref.disable_pin_until"; + // tviapp settings + private static final String PREF_TV_IAPP_STATES = "pref.tviapp_on"; + @Retention(RetentionPolicy.SOURCE) @IntDef({ CONTENT_RATING_LEVEL_NONE, @@ -242,4 +245,16 @@ public final class TvSettings { .putLong(PREF_DISABLE_PIN_UNTIL, timeMillis) .apply(); } + + public static boolean isTvIAppOn(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(PREF_TV_IAPP_STATES, false); + } + + public static void setTvIAppOn(Context context, boolean isOn) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(PREF_TV_IAPP_STATES, isOn) + .apply(); + } } diff --git a/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java index 5be62acb..c0263fa7 100644 --- a/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java +++ b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java @@ -56,9 +56,8 @@ public class MediaSessionWrapperTest { @Before public void setUp() { - pendingIntent = - PendingIntent.getActivity( - RuntimeEnvironment.application, TEST_REQUEST_CODE, new Intent(), 0); + pendingIntent = PendingIntent.getActivity(RuntimeEnvironment.application, TEST_REQUEST_CODE, + new Intent(), PendingIntent.FLAG_IMMUTABLE); mediaSessionWrapper = new MediaSessionWrapper(RuntimeEnvironment.application, pendingIntent) { @Override diff --git a/tuner/lint-baseline.xml b/tuner/lint-baseline.xml index a0db5e0b..f359c6b3 100644 --- a/tuner/lint-baseline.xml +++ b/tuner/lint-baseline.xml @@ -169,4 +169,20 @@ line="101"/> </issue> -</issues> + <issue + id="NewApi" + message="Call requires API level 26 (current min is 23): `new android.app.Notification.TvExtender`"> + <location + file="packages/apps/TV/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java" + line="416"/> + </issue> + + <issue + id="NewApi" + message="Cast from `TvExtender` to `Extender` requires API level 26 (current min is 23)"> + <location + file="packages/apps/TV/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java" + line="416"/> + </issue> + +</issues>
\ No newline at end of file diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java new file mode 100644 index 00000000..20c73de4 --- /dev/null +++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.samples.sampletunertvinput; + +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** Parser for ATSC PSIP sections */ +public class SampleTunerTvInputSectionParser { + private static final String TAG = "SampleTunerTvInput"; + private static final boolean DEBUG = true; + + public static final byte DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = (byte) 0xa0; + public static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00; + public static final byte MODE_UTF16 = (byte) 0x3f; + + /** + * Parses a single TVCT section, as defined in A/65 6.4 + * @param data, a ByteBuffer containing a single TVCT section which describes only one channel + * @return null if there is an error while parsing, the channel with parsed data otherwise + */ + public static TvctChannelInfo parseTvctSection(byte[] data) { + if (!checkValidPsipSection(data)) { + return null; + } + int numChannels = data[9] & 0xff; + if(numChannels != 1) { + Log.e(TAG, "parseTVCTSection expected 1 channel, found " + numChannels); + return null; + } + // TVCT Sections are a minimum of 16 bytes, with a minimum of 32 bytes per channel + if(data.length < 48) { + Log.e(TAG, "parseTVCTSection found section under minimum length"); + return null; + } + + // shortName begins at data[10] and ends at either the first stuffing + // UTF-16 character of value 0x0000, or at a length of 14 Bytes + int shortNameLength = 14; + for(int i = 0; i < 14; i += 2) { + int charValue = ((data[10 + i] & 0xff) << 8) | (data[10 + (i + 1)] & 0xff); + if (charValue == 0x0000) { + shortNameLength = i; + break; + } + } + // Data field positions are as defined by A/65 Section 6.4 for one channel + String name = new String(Arrays.copyOfRange(data, 10, 10 + shortNameLength), + StandardCharsets.UTF_16); + int majorNumber = ((data[24] & 0x0f) << 6) | ((data[25] & 0xff) >> 2); + int minorNumber = ((data[25] & 0x03) << 8) | (data[26] & 0xff); + if (DEBUG) { + Log.d(TAG, "parseTVCTSection found shortName: " + name + + " channel number: " + majorNumber + "-" + minorNumber); + } + int descriptorsLength = ((data[40] & 0x03) << 8) | (data[41] & 0xff); + List<TsDescriptor> descriptors = parseDescriptors(data, 42, 42 + descriptorsLength); + for (TsDescriptor descriptor : descriptors) { + if (descriptor instanceof ExtendedChannelNameDescriptor) { + ExtendedChannelNameDescriptor longNameDescriptor = + (ExtendedChannelNameDescriptor)descriptor; + name = longNameDescriptor.getLongChannelName(); + if (DEBUG) { + Log.d(TAG, "parseTVCTSection found longName: " + name); + } + } + } + + return new TvctChannelInfo(name, majorNumber, minorNumber); + } + + /** + * Parses a single EIT section, as defined in ATSC A/65 Section 6.5 + * @param data, a byte array containing a single EIT section which describes only one event + * @return {@code null} if there is an error while parsing, the event with parsed data otherwise + */ + public static EitEventInfo parseEitSection(byte[] data) { + if (!checkValidPsipSection(data)) { + return null; + } + int numEvents = data[9] & 0xff; + if(numEvents != 1) { + Log.e(TAG, "parseEitSection expected 1 event, found " + numEvents); + return null; + } + // EIT Sections are a minimum of 14 bytes, with a minimum of 12 bytes per event + if(data.length < 26) { + Log.e(TAG, "parseEitSection found section under minimum length"); + return null; + } + + // Data field positions are as defined by A/65 Section 6.5 for one event + int lengthInSeconds = ((data[16] & 0x0f) << 16) | ((data[17] & 0xff) << 8) + | (data[18] & 0xff); + int titleLength = data[19] & 0xff; + String titleText = parseMultipleStringStructure(data, 20, 20 + titleLength); + + if (DEBUG) { + Log.d(TAG, "parseEitSection found titleText: " + titleText + + " lengthInSeconds: " + lengthInSeconds); + } + return new EitEventInfo(titleText, lengthInSeconds); + } + + + // Descriptor data structure defined in ISO/IEC 13818-1 Section 2.6 + // Returns an empty list on parsing failures + private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) { + List<TsDescriptor> descriptors = new ArrayList<>(); + if (data.length < limit) { + Log.e(TAG, "parseDescriptors given limit larger than data"); + return descriptors; + } + int pos = offset; + while (pos + 1 < limit) { + int tag = data[pos] & 0xff; + int length = data[pos + 1] & 0xff; + if (length <= 0) { + continue; + } + pos += 2; + + if (limit < pos + length) { + Log.e(TAG, "parseDescriptors found descriptor with length longer than limit"); + break; + } + if (DEBUG) { + Log.d(TAG, "parseDescriptors found descriptor with tag: " + tag); + } + TsDescriptor descriptor = null; + switch ((byte) tag) { + case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME: + descriptor = parseExtendedChannelNameDescriptor(data, pos, pos + length); + break; + default: + break; + } + if (descriptor != null) { + descriptors.add(descriptor); + } + pos += length; + } + return descriptors; + } + + // ExtendedChannelNameDescriptor is defined in ATSC A/65 Section 6.9.4 as containing only + // a single MultipleStringStructure after its tag and length. + // @return {@code null} if parsing MultipleStringStructure fails + private static ExtendedChannelNameDescriptor parseExtendedChannelNameDescriptor(byte[] data, + int offset, int limit) { + String channelName = parseMultipleStringStructure(data, offset, limit); + return channelName == null ? null : new ExtendedChannelNameDescriptor(channelName); + } + + // MultipleStringStructure is defined in ATSC A/65 Section 6.10 + // Returns first string segment with supported compression and mode + // @return {@code null} on invalid data or no supported string segments + private static String parseMultipleStringStructure(byte[] data, int offset, int limit) { + if (limit < offset + 8) { + Log.e(TAG, "parseMultipleStringStructure given too little data"); + return null; + } + + int numStrings = data[offset] & 0xff; + if (numStrings <= 0) { + Log.e(TAG, "parseMultipleStringStructure found no strings"); + return null; + } + int pos = offset + 1; + for (int i = 0; i < numStrings; i++) { + if (limit < pos + 4) { + Log.e(TAG, "parseMultipleStringStructure ran out of data"); + return null; + } + int numSegments = data[pos + 3] & 0xff; + pos += 4; + for (int j = 0; j < numSegments; j++) { + if (limit < pos + 3) { + Log.e(TAG, "parseMultipleStringStructure ran out of data"); + return null; + } + int compressionType = data[pos] & 0xff; + int mode = data[pos + 1] & 0xff; + int numBytes = data[pos + 2] & 0xff; + pos += 3; + if (data.length < pos + numBytes) { + Log.e(TAG, "parseMultipleStringStructure ran out of data"); + return null; + } + if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION && mode == MODE_UTF16) { + return new String(data, pos, numBytes, StandardCharsets.UTF_16); + } + pos += numBytes; + } + } + + Log.e(TAG, "parseMultipleStringStructure found no supported segments"); + return null; + } + + private static boolean checkValidPsipSection(byte[] data) { + if (data.length < 13) { + Log.e(TAG, "Section was too small"); + return false; + } + if ((data[0] & 0xff) == 0xff) { + // Should clear stuffing bytes as detailed by H222.0 section 2.4.4. + Log.e(TAG, "Unexpected stuffing bytes while parsing section"); + return false; + } + int sectionLength = (((data[1] & 0x0f) << 8) | (data[2] & 0xff)) + 3; + if (sectionLength != data.length) { + Log.e(TAG, "Length mismatch while parsing section"); + return false; + } + int sectionNumber = data[6] & 0xff; + int lastSectionNumber = data[7] & 0xff; + if(sectionNumber > lastSectionNumber) { + Log.e(TAG, "Found sectionNumber > lastSectionNumber while parsing section"); + return false; + } + // TODO: Check CRC 32/MPEG for validity + return true; + } + + // Contains the portion of the data contained in the TVCT used by + // our SampleTunerTvInputSetupActivity + public static class TvctChannelInfo { + private final String mChannelName; + private final int mMajorChannelNumber; + private final int mMinorChannelNumber; + + public TvctChannelInfo( + String channelName, + int majorChannelNumber, + int minorChannelNumber) { + mChannelName = channelName; + mMajorChannelNumber = majorChannelNumber; + mMinorChannelNumber = minorChannelNumber; + } + + public String getChannelName() { + return mChannelName; + } + + public int getMajorChannelNumber() { + return mMajorChannelNumber; + } + + public int getMinorChannelNumber() { + return mMinorChannelNumber; + } + + @Override + public String toString() { + return String.format( + Locale.US, + "ChannelName: %s ChannelNumber: %d-%d", + mChannelName, + mMajorChannelNumber, + mMinorChannelNumber); + } + } + + /** + * Contains the portion of the data contained in the EIT used by + * our SampleTunerTvInputService + */ + public static class EitEventInfo { + private final String mEventTitle; + private final int mLengthSeconds; + + public EitEventInfo( + String eventTitle, + int lengthSeconds) { + mEventTitle = eventTitle; + mLengthSeconds = lengthSeconds; + } + + public String getEventTitle() { + return mEventTitle; + } + + public int getLengthSeconds() { + return mLengthSeconds; + } + + @Override + public String toString() { + return String.format( + Locale.US, + "Event Title: %s Length in Seconds: %d", + mEventTitle, + mLengthSeconds); + } + } + + /** + * A base class for TS descriptors + * For details of their structure, see ATSC A/65 Section 6.9 + */ + public abstract static class TsDescriptor { + public abstract int getTag(); + } + + public static class ExtendedChannelNameDescriptor extends TsDescriptor { + private final String mLongChannelName; + + public ExtendedChannelNameDescriptor(String longChannelName) { + mLongChannelName = longChannelName; + } + + @Override + public int getTag() { + return DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME; + } + + public String getLongChannelName() { + return mLongChannelName; + } + } +} diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java index 03e79650..d59ccd9d 100644 --- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java +++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java @@ -1,34 +1,31 @@ package com.android.tv.samples.sampletunertvinput; +import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING; import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; -import android.media.MediaCodec.LinearBlock; import android.media.MediaFormat; +import android.media.tv.TvContract; import android.media.tv.tuner.dvr.DvrPlayback; import android.media.tv.tuner.dvr.DvrSettings; -import android.media.tv.tuner.filter.AvSettings; import android.media.tv.tuner.filter.Filter; import android.media.tv.tuner.filter.FilterCallback; import android.media.tv.tuner.filter.FilterEvent; import android.media.tv.tuner.filter.MediaEvent; -import android.media.tv.tuner.filter.TsFilterConfiguration; -import android.media.tv.tuner.frontend.AtscFrontendSettings; -import android.media.tv.tuner.frontend.DvbtFrontendSettings; -import android.media.tv.tuner.frontend.FrontendSettings; -import android.media.tv.tuner.frontend.OnTuneEventListener; import android.media.tv.tuner.Tuner; import android.media.tv.TvInputService; +import android.media.tv.tuner.filter.SectionEvent; import android.net.Uri; import android.os.Handler; -import android.os.HandlerExecutor; -import android.os.ParcelFileDescriptor; import android.util.Log; import android.view.Surface; -import java.io.File; -import java.io.FileNotFoundException; + +import com.android.tv.common.util.Clock; + import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayDeque; @@ -42,40 +39,31 @@ public class SampleTunerTvInputService extends TvInputService { private static final String TAG = "SampleTunerTvInput"; private static final boolean DEBUG = true; - private static final int AUDIO_TPID = 257; - private static final int VIDEO_TPID = 256; - private static final int STATUS_MASK = 0xf; - private static final int LOW_THRESHOLD = 0x1000; - private static final int HIGH_THRESHOLD = 0x07fff; - private static final int FREQUENCY = 578000; - private static final int FILTER_BUFFER_SIZE = 16000000; - private static final int DVR_BUFFER_SIZE = 4000000; - private static final int INPUT_FILE_MAX_SIZE = 700000; - private static final int PACKET_SIZE = 188; - private static final int TIMEOUT_US = 100000; private static final boolean SAVE_DATA = false; - private static final String ES_FILE_NAME = "test.es"; + private static final boolean USE_DVR = true; + private static final String MEDIA_INPUT_FILE_NAME = "media.ts"; private static final MediaFormat VIDEO_FORMAT; static { // format extracted for the specific input file - VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 320, 240); + VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 360); VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1); - VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333); - VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32); + VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 10000000); + VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 256); VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536); ByteBuffer csd = ByteBuffer.wrap( - new byte[] {0, 0, 0, 1, 103, 66, -64, 20, -38, 5, 7, -24, 64, 0, 0, 3, 0, 64, 0, - 0, 15, 35, -59, 10, -88}); + new byte[] {0, 0, 0, 1, 103, 66, -64, 30, -39, 1, -32, -65, -27, -64, 68, 0, 0, 3, + 0, 4, 0, 0, 3, 0, -16, 60, 88, -71, 32}); VIDEO_FORMAT.setByteBuffer("csd-0", csd); - csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -50, 60, -128}); + csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -53, -125, -53, 32}); VIDEO_FORMAT.setByteBuffer("csd-1", csd); } public static final String INPUT_ID = "com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService"; private String mSessionId; + private Uri mChannelUri; @Override public TvInputSessionImpl onCreateSession(String inputId, String sessionId) { @@ -89,6 +77,9 @@ public class SampleTunerTvInputService extends TvInputService { @Override public TvInputSessionImpl onCreateSession(String inputId) { + if (DEBUG) { + Log.d(TAG, "onCreateSession(inputId=" + inputId + ")"); + } return new TvInputSessionImpl(this); } @@ -100,12 +91,16 @@ public class SampleTunerTvInputService extends TvInputService { private Surface mSurface; private Filter mAudioFilter; private Filter mVideoFilter; + private Filter mSectionFilter; private DvrPlayback mDvr; private Tuner mTuner; private MediaCodec mMediaCodec; private Thread mDecoderThread; - private Deque<MediaEvent> mDataQueue; - private List<MediaEvent> mSavedData; + private Deque<MediaEventData> mDataQueue; + private List<MediaEventData> mSavedData; + private long mCurrentLoopStartTimeUs = 0; + private long mLastFramePtsUs = 0; + private boolean mVideoAvailable; private boolean mDataReady = false; @@ -133,6 +128,9 @@ public class SampleTunerTvInputService extends TvInputService { if (mVideoFilter != null) { mVideoFilter.close(); } + if (mSectionFilter != null) { + mSectionFilter.close(); + } if (mDvr != null) { mDvr.close(); mDvr = null; @@ -170,7 +168,11 @@ public class SampleTunerTvInputService extends TvInputService { Log.e(TAG, "null codec!"); return false; } + mChannelUri = uri; mHandler = new Handler(); + mVideoAvailable = false; + notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING); + mDecoderThread = new Thread( this::decodeInternal, @@ -186,139 +188,79 @@ public class SampleTunerTvInputService extends TvInputService { } } - private Filter audioFilter() { - Filter audioFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, - FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler), - new FilterCallback() { - @Override - public void onFilterEvent(Filter filter, FilterEvent[] events) { - if (DEBUG) { - Log.d(TAG, "onFilterEvent audio, size=" + events.length); - } - for (int i = 0; i < events.length; i++) { - if (DEBUG) { - Log.d(TAG, "events[" + i + "] is " - + events[i].getClass().getSimpleName()); - } - } + private FilterCallback videoFilterCallback() { + return new FilterCallback() { + @Override + public void onFilterEvent(Filter filter, FilterEvent[] events) { + if (DEBUG) { + Log.d(TAG, "onFilterEvent video, size=" + events.length); + } + for (int i = 0; i < events.length; i++) { + if (DEBUG) { + Log.d(TAG, "events[" + i + "] is " + + events[i].getClass().getSimpleName()); } + if (events[i] instanceof MediaEvent) { + MediaEvent me = (MediaEvent) events[i]; - @Override - public void onFilterStatusChanged(Filter filter, int status) { - if (DEBUG) { - Log.d(TAG, "onFilterEvent audio, status=" + status); + MediaEventData storedEvent = MediaEventData.generateEventData(me); + if (storedEvent == null) { + continue; + } + mDataQueue.add(storedEvent); + if (SAVE_DATA) { + mSavedData.add(storedEvent); } } - }); - AvSettings settings = - AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build(); - audioFilter.configure( - TsFilterConfiguration.builder().setTpid(AUDIO_TPID) - .setSettings(settings).build()); - return audioFilter; + } + } + + @Override + public void onFilterStatusChanged(Filter filter, int status) { + if (DEBUG) { + Log.d(TAG, "onFilterEvent video, status=" + status); + } + if (status == Filter.STATUS_DATA_READY) { + mDataReady = true; + } + } + }; } - private Filter videoFilter() { - Filter videoFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_VIDEO, - FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler), - new FilterCallback() { - @Override - public void onFilterEvent(Filter filter, FilterEvent[] events) { - if (DEBUG) { - Log.d(TAG, "onFilterEvent video, size=" + events.length); - } - for (int i = 0; i < events.length; i++) { - if (DEBUG) { - Log.d(TAG, "events[" + i + "] is " - + events[i].getClass().getSimpleName()); - } - if (events[i] instanceof MediaEvent) { - MediaEvent me = (MediaEvent) events[i]; - mDataQueue.add(me); - if (SAVE_DATA) { - mSavedData.add(me); - } - } - } + private FilterCallback sectionFilterCallback() { + return new FilterCallback() { + @Override + public void onFilterEvent(Filter filter, FilterEvent[] events) { + if (DEBUG) { + Log.d(TAG, "onFilterEvent section, size=" + events.length); + } + for (int i = 0; i < events.length; i++) { + if (DEBUG) { + Log.d(TAG, "events[" + i + "] is " + + events[i].getClass().getSimpleName()); } - - @Override - public void onFilterStatusChanged(Filter filter, int status) { + if (events[i] instanceof SectionEvent) { + SectionEvent sectionEvent = (SectionEvent) events[i]; + int dataSize = (int)sectionEvent.getDataLengthLong(); if (DEBUG) { - Log.d(TAG, "onFilterEvent video, status=" + status); - } - if (status == Filter.STATUS_DATA_READY) { - mDataReady = true; + Log.d(TAG, "section dataSize:" + dataSize); } - } - }); - AvSettings settings = - AvSettings.builder(Filter.TYPE_TS, false).setPassthrough(false).build(); - videoFilter.configure( - TsFilterConfiguration.builder().setTpid(VIDEO_TPID) - .setSettings(settings).build()); - return videoFilter; - } - private DvrPlayback dvrPlayback() { - DvrPlayback dvr = mTuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(mHandler), - status -> { - if (DEBUG) { - Log.d(TAG, "onPlaybackStatusChanged status=" + status); + byte[] data = new byte[dataSize]; + filter.read(data, 0, dataSize); + + handleSection(data); } - }); - int res = dvr.configure( - DvrSettings.builder() - .setStatusMask(STATUS_MASK) - .setLowThreshold(LOW_THRESHOLD) - .setHighThreshold(HIGH_THRESHOLD) - .setDataFormat(DvrSettings.DATA_FORMAT_ES) - .setPacketSize(PACKET_SIZE) - .build()); - if (DEBUG) { - Log.d(TAG, "config res=" + res); - } - String testFile = mContext.getFilesDir().getAbsolutePath() + "/" + ES_FILE_NAME; - File file = new File(testFile); - if (file.exists()) { - try { - dvr.setFileDescriptor( - ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)); - } catch (FileNotFoundException e) { - Log.e(TAG, "Failed to create FD"); + } } - } else { - Log.w(TAG, "File not existing"); - } - return dvr; - } - private void tune() { - DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder() - .setFrequency(FREQUENCY) - .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO) - .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ) - .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO) - .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO) - .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) - .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) - .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO) - .setHighPriority(true) - .setStandard(DvbtFrontendSettings.STANDARD_T) - .build(); - mTuner.setOnTuneEventListener(new HandlerExecutor(mHandler), new OnTuneEventListener() { @Override - public void onTuneEvent(int tuneEvent) { - if (DEBUG) { - Log.d(TAG, "onTuneEvent " + tuneEvent); - } - long read = mDvr.read(INPUT_FILE_MAX_SIZE); + public void onFilterStatusChanged(Filter filter, int status) { if (DEBUG) { - Log.d(TAG, "read=" + read); + Log.d(TAG, "onFilterStatusChanged section, status=" + status); } } - }); - mTuner.tune(feSettings); + }; } private boolean initCodec() { @@ -335,6 +277,7 @@ public class SampleTunerTvInputService extends TvInputService { if (mMediaCodec == null) { Log.e(TAG, "null codec!"); + mVideoAvailable = false; notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN); return false; } @@ -347,14 +290,26 @@ public class SampleTunerTvInputService extends TvInputService { mTuner = new Tuner(mContext, mSessionId, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE); - mAudioFilter = audioFilter(); - mVideoFilter = videoFilter(); + mAudioFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler, + SampleTunerTvInputUtils.createDefaultLoggingFilterCallback("audio"), true); + mVideoFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler, + videoFilterCallback(), false); + mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, mHandler, + sectionFilterCallback()); mAudioFilter.start(); mVideoFilter.start(); - // use dvr playback to feed the data on platform without physical tuner - mDvr = dvrPlayback(); - tune(); - mDvr.start(); + mSectionFilter.start(); + + // Dvr Playback can be used to read a file instead of relying on physical tuner + if (USE_DVR) { + mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, mHandler, + DvrSettings.DATA_FORMAT_TS); + SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr, + MEDIA_INPUT_FILE_NAME); + mDvr.start(); + } else { + SampleTunerTvInputUtils.tune(mTuner, mHandler); + } mMediaCodec.start(); try { @@ -369,7 +324,10 @@ public class SampleTunerTvInputService extends TvInputService { mDataQueue.pollFirst(); } } - if (SAVE_DATA) { + else if (SAVE_DATA) { + if (DEBUG) { + Log.d(TAG, "Adding saved data to data queue"); + } mDataQueue.addAll(mSavedData); } } @@ -378,24 +336,50 @@ public class SampleTunerTvInputService extends TvInputService { } } - private boolean handleDataBuffer(MediaEvent mediaEvent) { - if (mediaEvent.getLinearBlock() == null) { - if (DEBUG) Log.d(TAG, "getLinearBlock() == null"); - return true; + private void handleSection(byte[] data) { + SampleTunerTvInputSectionParser.EitEventInfo eventInfo = + SampleTunerTvInputSectionParser.parseEitSection(data); + if (eventInfo == null) { + Log.e(TAG, "Did not receive event info from parser"); + return; + } + + // We assume that our program starts at the current time + long startTimeMs = Clock.SYSTEM.currentTimeMillis(); + long endTimeMs = startTimeMs + ((long)eventInfo.getLengthSeconds() * 1000); + + // Remove any other programs which conflict with our start and end time + Uri conflictsUri = + TvContract.buildProgramsUriForChannel(mChannelUri, startTimeMs, endTimeMs); + int programsDeleted = mContext.getContentResolver().delete(conflictsUri, null, null); + if (DEBUG) { + Log.d(TAG, "Deleted " + programsDeleted + " conflicting program(s)"); + } + + // Insert our new program into the newly opened time slot + ContentValues values = new ContentValues(); + values.put(TvContract.Programs.COLUMN_CHANNEL_ID, ContentUris.parseId(mChannelUri)); + values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeMs); + values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeMs); + values.put(TvContract.Programs.COLUMN_TITLE, eventInfo.getEventTitle()); + values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, ""); + if (DEBUG) { + Log.d(TAG, "Inserting program with values: " + values); } + mContext.getContentResolver().insert(TvContract.Programs.CONTENT_URI, values); + } + + private boolean handleDataBuffer(MediaEventData mediaEventData) { boolean success = false; - LinearBlock block = mediaEvent.getLinearBlock(); - if (queueCodecInputBuffer(block, mediaEvent.getDataLength(), mediaEvent.getOffset(), - mediaEvent.getPts())) { + if (queueCodecInputBuffer(mediaEventData.getData(), mediaEventData.getDataSize(), + mediaEventData.getPts())) { releaseCodecOutputBuffer(); success = true; } - mediaEvent.release(); return success; } - private boolean queueCodecInputBuffer(LinearBlock block, long sampleSize, - long offset, long pts) { + private boolean queueCodecInputBuffer(byte[] data, int size, long pts) { int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US); if (res >= 0) { ByteBuffer buffer = mMediaCodec.getInputBuffer(res); @@ -403,41 +387,19 @@ public class SampleTunerTvInputService extends TvInputService { throw new RuntimeException("Null decoder input buffer"); } - ByteBuffer data = block.map(); - if (offset > 0 && offset < data.limit()) { - data.position((int) offset); - } else { - data.position(0); - } - if (DEBUG) { Log.d( TAG, "Decoder: Send data to decoder." - + " Sample size=" - + sampleSize + " pts=" + pts - + " limit=" - + data.limit() - + " pos=" - + data.position() + " size=" - + (data.limit() - data.position())); + + size); } // fill codec input buffer - int size = sampleSize > data.limit() ? data.limit() : (int) sampleSize; - if (DEBUG) Log.d(TAG, "limit " + data.limit() + " sampleSize " + sampleSize); - if (data.hasArray()) { - Log.d(TAG, "hasArray"); - buffer.put(data.array(), 0, size); - } else { - byte[] array = new byte[size]; - data.get(array, 0, size); - buffer.put(array, 0, size); - } + buffer.put(data, 0, size); - mMediaCodec.queueInputBuffer(res, 0, (int) sampleSize, pts, 0); + mMediaCodec.queueInputBuffer(res, 0, size, pts, 0); } else { if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res); return false; @@ -450,10 +412,43 @@ public class SampleTunerTvInputService extends TvInputService { BufferInfo bufferInfo = new BufferInfo(); int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); if (res >= 0) { - mMediaCodec.releaseOutputBuffer(res, true); - notifyVideoAvailable(); + long currentFramePtsUs = bufferInfo.presentationTimeUs; + + // We know we are starting a new loop if the loop time is not set or if + // the current frame is before the last frame + if (mCurrentLoopStartTimeUs == 0 || currentFramePtsUs < mLastFramePtsUs) { + mCurrentLoopStartTimeUs = System.nanoTime() / 1000; + } + mLastFramePtsUs = currentFramePtsUs; + + long desiredUs = mCurrentLoopStartTimeUs + currentFramePtsUs; + long nowUs = System.nanoTime() / 1000; + long sleepTimeUs = desiredUs - nowUs; + if (DEBUG) { - Log.d(TAG, "notifyVideoAvailable"); + Log.d(TAG, "currentFramePts: " + currentFramePtsUs + + " sleeping for: " + sleepTimeUs); + } + if (sleepTimeUs > 0) { + try { + Thread.sleep( + /* millis */ sleepTimeUs / 1000, + /* nanos */ (int) (sleepTimeUs % 1000) * 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (DEBUG) { + Log.d(TAG, "InterruptedException:\n" + Log.getStackTraceString(e)); + } + return; + } + } + mMediaCodec.releaseOutputBuffer(res, true); + if (!mVideoAvailable) { + mVideoAvailable = true; + notifyVideoAvailable(); + if (DEBUG) { + Log.d(TAG, "notifyVideoAvailable"); + } } } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat format = mMediaCodec.getOutputFormat(); @@ -472,4 +467,75 @@ public class SampleTunerTvInputService extends TvInputService { } } + + /** + * MediaEventData is a helper class which is used to hold the data within MediaEvents + * locally in our Java code, instead of in the position allocated by our native code + */ + public static class MediaEventData { + private final long mPts; + private final int mDataSize; + private final byte[] mData; + + public MediaEventData(long pts, int dataSize, byte[] data) { + mPts = pts; + mDataSize = dataSize; + mData = data; + } + + /** + * Parses a MediaEvent, including copying its data and freeing the underlying LinearBlock + * @return {@code null} if the event has no LinearBlock + */ + public static MediaEventData generateEventData(MediaEvent event) { + if(event.getLinearBlock() == null) { + if (DEBUG) { + Log.d(TAG, "MediaEvent had null LinearBlock"); + } + return null; + } + + ByteBuffer memoryBlock = event.getLinearBlock().map(); + int eventOffset = (int)event.getOffset(); + int eventDataLength = (int)event.getDataLength(); + if (DEBUG) { + Log.d(TAG, "MediaEvent has length=" + eventDataLength + + " offset=" + eventOffset + + " capacity=" + memoryBlock.capacity() + + " limit=" + memoryBlock.limit()); + } + if (eventOffset < 0 || eventDataLength < 0 || eventOffset >= memoryBlock.limit()) { + if (DEBUG) { + Log.e(TAG, "MediaEvent length or offset was invalid"); + } + event.getLinearBlock().recycle(); + event.release(); + return null; + } + // We allow the case of eventOffset + eventDataLength > memoryBlock.limit() + // When it occurs, we read until memoryBlock.limit + int dataSize = Math.min(eventDataLength, memoryBlock.limit() - eventOffset); + memoryBlock.position(eventOffset); + + byte[] memoryData = new byte[dataSize]; + memoryBlock.get(memoryData, 0, dataSize); + MediaEventData eventData = new MediaEventData(event.getPts(), dataSize, memoryData); + + event.getLinearBlock().recycle(); + event.release(); + return eventData; + } + + public long getPts() { + return mPts; + } + + public int getDataSize() { + return mDataSize; + } + + public byte[] getData() { + return mData; + } + } } diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java index b932b605..4774243e 100644 --- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java +++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java @@ -3,48 +3,158 @@ package com.android.tv.samples.sampletunertvinput; import android.app.Activity; import android.content.Intent; import android.media.tv.TvInputInfo; +import android.media.tv.TvInputService; +import android.media.tv.tuner.Tuner; +import android.media.tv.tuner.dvr.DvrPlayback; +import android.media.tv.tuner.dvr.DvrSettings; +import android.media.tv.tuner.filter.Filter; +import android.media.tv.tuner.filter.FilterCallback; +import android.media.tv.tuner.filter.FilterEvent; +import android.media.tv.tuner.filter.SectionEvent; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.android.tv.common.util.Clock; import com.android.tv.testing.data.ChannelInfo; import com.android.tv.testing.data.ChannelUtils; import com.android.tv.testing.data.ProgramInfo; +import com.android.tv.testing.data.ProgramUtils; + import java.util.Collections; +import java.util.Locale; +import java.util.concurrent.TimeUnit; /** Setup activity for SampleTunerTvInput */ public class SampleTunerTvInputSetupActivity extends Activity { + private static final String TAG = "SampleTunerTvInput"; + private static final boolean DEBUG = true; + + private static final boolean USE_DVR = true; + private static final String SETUP_INPUT_FILE_NAME = "setup.ts"; + + private Tuner mTuner; + private DvrPlayback mDvr; + private Filter mSectionFilter; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + initTuner(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mTuner != null) { + mTuner.close(); + mTuner = null; + } + if (mDvr != null) { + mDvr.close(); + mDvr = null; + } + if (mSectionFilter != null) { + mSectionFilter.close(); + mSectionFilter = null; + } + } + + private void setChannel(byte[] sectionData) { + SampleTunerTvInputSectionParser.TvctChannelInfo channelInfo = + SampleTunerTvInputSectionParser.parseTvctSection(sectionData); + + String channelNumber = ""; + String channelName = ""; + + if(channelInfo == null) { + Log.e(TAG, "Did not receive channel description from parser"); + } else { + channelNumber = String.format(Locale.US, "%d-%d", channelInfo.getMajorChannelNumber(), + channelInfo.getMinorChannelNumber()); + channelName = channelInfo.getChannelName(); + } + ChannelInfo channel = - new ChannelInfo.Builder() - .setNumber("1-1") - .setName("Sample Channel") - .setLogoUrl( - ChannelInfo.getUriStringForChannelLogo(this, 100)) - .setOriginalNetworkId(1) - .setVideoWidth(640) - .setVideoHeight(480) - .setAudioChannel(2) - .setAudioLanguageCount(1) - .setHasClosedCaption(false) - .setProgram( - new ProgramInfo( - "Sample Program", - "", - 0, - 0, - ProgramInfo.GEN_POSTER, - "Sample description", - ProgramInfo.GEN_DURATION, - null, - ProgramInfo.GEN_GENRE, - null)) - .build(); + new ChannelInfo.Builder() + .setNumber(channelNumber) + .setName(channelName) + .setLogoUrl( + ChannelInfo.getUriStringForChannelLogo(this, 100)) + .setOriginalNetworkId(1) + .setVideoWidth(640) + .setVideoHeight(480) + .setAudioChannel(2) + .setAudioLanguageCount(1) + .setHasClosedCaption(false) + .build(); Intent intent = getIntent(); String inputId = intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID); ChannelUtils.updateChannels(this, inputId, Collections.singletonList(channel)); + ProgramUtils.updateProgramForAllChannelsOf(this, inputId, Clock.SYSTEM, + TimeUnit.DAYS.toMillis(1)); + setResult(Activity.RESULT_OK); finish(); } + private FilterCallback sectionFilterCallback() { + return new FilterCallback() { + @Override + public void onFilterEvent(Filter filter, FilterEvent[] events) { + if (DEBUG) { + Log.d(TAG, "onFilterEvent setup section, size=" + events.length); + } + for (int i = 0; i < events.length; i++) { + if (DEBUG) { + Log.d(TAG, "events[" + i + "] is " + + events[i].getClass().getSimpleName()); + } + if (events[i] instanceof SectionEvent) { + SectionEvent sectionEvent = (SectionEvent) events[i]; + int dataSize = (int)sectionEvent.getDataLengthLong(); + if (DEBUG) { + Log.d(TAG, "section dataSize:" + dataSize); + } + + byte[] data = new byte[dataSize]; + filter.read(data, 0, dataSize); + + setChannel(data); + } + } + } + + @Override + public void onFilterStatusChanged(Filter filter, int status) { + if (DEBUG) { + Log.d(TAG, "onFilterStatusChanged setup section, status=" + status); + } + } + }; + } + + private void initTuner() { + mTuner = new Tuner(getApplicationContext(), null, + TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE); + Handler handler = new Handler(Looper.myLooper()); + + mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, handler, + sectionFilterCallback()); + mSectionFilter.start(); + + // Dvr Playback can be used to read a file instead of relying on physical tuner + if (USE_DVR) { + mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, handler, + DvrSettings.DATA_FORMAT_TS); + SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr, + SETUP_INPUT_FILE_NAME); + mDvr.start(); + } else { + SampleTunerTvInputUtils.tune(mTuner, handler); + } + } + } diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java new file mode 100644 index 00000000..9638f33a --- /dev/null +++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.samples.sampletunertvinput; + +import android.content.Context; +import android.media.tv.tuner.Tuner; +import android.media.tv.tuner.dvr.DvrPlayback; +import android.media.tv.tuner.dvr.DvrSettings; +import android.media.tv.tuner.filter.AvSettings; +import android.media.tv.tuner.filter.Filter; +import android.media.tv.tuner.filter.FilterCallback; +import android.media.tv.tuner.filter.FilterEvent; +import android.media.tv.tuner.filter.SectionSettingsWithSectionBits; +import android.media.tv.tuner.filter.TsFilterConfiguration; +import android.media.tv.tuner.frontend.DvbtFrontendSettings; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; + +public class SampleTunerTvInputUtils { + private static final String TAG = "SampleTunerTvInput"; + private static final boolean DEBUG = true; + + private static final int AUDIO_TPID = 257; + private static final int VIDEO_TPID = 256; + private static final int SECTION_TPID = 255; + private static final int FILTER_BUFFER_SIZE = 16000000; + + private static final int STATUS_MASK = 0xf; + private static final int LOW_THRESHOLD = 0x1000; + private static final int HIGH_THRESHOLD = 0x07fff; + private static final int DVR_BUFFER_SIZE = 4000000; + private static final int PACKET_SIZE = 188; + private static final long FREQUENCY = 578000; + private static final int INPUT_FILE_MAX_SIZE = 1000000; + + public static DvrPlayback configureDvrPlayback(Tuner tuner, Handler handler, int dataFormat) { + DvrPlayback dvr = tuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(handler), + status -> { + if (DEBUG) { + Log.d(TAG, "onPlaybackStatusChanged status=" + status); + } + }); + int res = dvr.configure( + DvrSettings.builder() + .setStatusMask(STATUS_MASK) + .setLowThreshold(LOW_THRESHOLD) + .setHighThreshold(HIGH_THRESHOLD) + .setDataFormat(dataFormat) + .setPacketSize(PACKET_SIZE) + .build()); + if (DEBUG) { + Log.d(TAG, "config res=" + res); + } + return dvr; + } + + public static void readFilePlaybackInput(Context context, DvrPlayback dvr, String fileName) { + String testFile = context.getFilesDir().getAbsolutePath() + "/" + fileName; + File file = new File(testFile); + if (file.exists()) { + try { + dvr.setFileDescriptor( + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Failed to create FD"); + } + } else { + Log.w(TAG, "File not existing"); + } + + long read = dvr.read(INPUT_FILE_MAX_SIZE); + if (DEBUG) { + Log.d(TAG, "read=" + read); + } + } + + public static void tune(Tuner tuner, Handler handler) { + DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder() + .setFrequencyLong(FREQUENCY) + .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO) + .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ) + .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO) + .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO) + .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) + .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) + .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO) + .setHighPriority(true) + .setStandard(DvbtFrontendSettings.STANDARD_T) + .build(); + + tuner.setOnTuneEventListener(new HandlerExecutor(handler), tuneEvent -> { + if (DEBUG) { + Log.d(TAG, "onTuneEvent " + tuneEvent); + } + }); + + tuner.tune(feSettings); + } + + public static Filter createSectionFilter(Tuner tuner, Handler handler, + FilterCallback callback) { + Filter sectionFilter = tuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_SECTION, + FILTER_BUFFER_SIZE, new HandlerExecutor(handler), callback); + + SectionSettingsWithSectionBits settings = SectionSettingsWithSectionBits + .builder(Filter.TYPE_TS).build(); + + sectionFilter.configure( + TsFilterConfiguration.builder().setTpid(SECTION_TPID) + .setSettings(settings).build()); + + return sectionFilter; + } + + public static Filter createAvFilter(Tuner tuner, Handler handler, + FilterCallback callback, boolean isAudio) { + Filter avFilter = tuner.openFilter(Filter.TYPE_TS, + isAudio ? Filter.SUBTYPE_AUDIO : Filter.SUBTYPE_VIDEO, + FILTER_BUFFER_SIZE, + new HandlerExecutor(handler), + callback); + + AvSettings settings = + AvSettings.builder(Filter.TYPE_TS, isAudio).setPassthrough(false).build(); + avFilter.configure( + TsFilterConfiguration.builder(). + setTpid(isAudio ? AUDIO_TPID : VIDEO_TPID) + .setSettings(settings).build()); + return avFilter; + } + + public static FilterCallback createDefaultLoggingFilterCallback(String filterType) { + return new FilterCallback() { + @Override + public void onFilterEvent(Filter filter, FilterEvent[] events) { + if (DEBUG) { + Log.d(TAG, "onFilterEvent " + filterType + ", size=" + events.length); + } + for (int i = 0; i < events.length; i++) { + if (DEBUG) { + Log.d(TAG, "events[" + i + "] is " + + events[i].getClass().getSimpleName()); + } + } + } + + @Override + public void onFilterStatusChanged(Filter filter, int status) { + if (DEBUG) { + Log.d(TAG, "onFilterStatusChanged " + filterType + ", status=" + status); + } + } + }; + } +} diff --git a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java index 05026907..99f3e6dc 100644 --- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java +++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java @@ -463,8 +463,8 @@ public abstract class BaseTunerSetupActivity extends SetupActivity { */ private static PendingIntent createPendingIntentForSetupActivity( Context context, Intent tunerSetupIntent) { - return PendingIntent.getActivity( - context, 0, tunerSetupIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity(context, 0, tunerSetupIntent, + PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE); } /** Creates {@link Tuner} instances in a worker thread * */ |