diff options
author | Xin Li <delphij@google.com> | 2019-07-01 21:00:07 +0000 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2019-07-01 21:00:07 +0000 |
commit | effbc99d77bb465246a80f96b2c0128885783f0b (patch) | |
tree | f4ba89447e2d7ac3355508e583a5cf0574c29af5 | |
parent | c8b794291c5b8b5be03d740355ab33f2df79a76e (diff) | |
parent | d12bfa398c56027290a9e6e4fd14f635458ec581 (diff) | |
download | TV-temp_140451723.tar.gz |
DO NOT MERGE - Merge qt-dev-plus-aosp-without-vendor (5699924) into stage-aosp-mastertemp_140451723
Bug: 134405016
Change-Id: If83c4123ed715e08bc37b8f5772adc84e86298af
586 files changed, 17584 insertions, 8467 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 00000000..42686365 --- /dev/null +++ b/Android.bp @@ -0,0 +1,96 @@ +// +// Copyright (C) 2015 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. +// + +version_name = "1.20-asop" +version_code = "417000328" + +android_app { + name: "LiveTv", + + srcs: ["src/**/*.java"], + + // TODO(b/122608868) turn proguard back on + optimize: { + enabled: false, + }, + + // It is required for com.android.providers.tv.permission.ALL_EPG_DATA + privileged: true, + + sdk_version: "system_current", + min_sdk_version: "23", // M + + resource_dirs: [ + "res", + "material_res", + + ], + + libs: ["tv-guava-android-jar"], + + static_libs: [ + "android-support-annotations", + "android-support-compat", + "android-support-core-ui", + "androidx.tvprovider_tvprovider", + "android-support-v4", + "android-support-v7-appcompat", + "android-support-v7-palette", + "android-support-v7-preference", + "android-support-v7-recyclerview", + "android-support-v14-preference", + "android-support-v17-leanback", + "android-support-v17-preference-leanback", + "jsr330", + "live-channels-partner-support", + "live-tv-tuner-proto", + "live-tv-tuner", + "tv-auto-value-jar", + "tv-auto-factory-jar", + "tv-common", + "tv-error-prone-annotations-jar", + "tv-lib-dagger", + "tv-lib-exoplayer", + "tv-lib-exoplayer-v2-core", + "tv-lib-dagger-android", + ], + + plugins: [ + "tv-auto-value", + "tv-auto-factory", + "tv-lib-dagger-android-processor", + "tv-lib-dagger-compiler", + ], + + javacflags: [ + "-Xlint:deprecation", + "-Xlint:unchecked", + ], + + aaptflags: [ + "--version-name", + version_name, + + "--version-code", + version_code, + + "--extra-packages", + "com.android.tv.tuner", + + "--extra-packages", + "com.android.tv.common", + ], +} diff --git a/Android.mk b/Android.mk deleted file mode 100644 index 351d8b57..00000000 --- a/Android.mk +++ /dev/null @@ -1,102 +0,0 @@ -# -# Copyright (C) 2015 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. -# - -LOCAL_PATH:= $(call my-dir) - -include $(CLEAR_VARS) - - -LOCAL_MODULE_TAGS := optional - -include $(LOCAL_PATH)/version.mk - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_PACKAGE_NAME := LiveTv - -# It is required for com.android.providers.tv.permission.ALL_EPG_DATA -LOCAL_PRIVILEGED_MODULE := true - -LOCAL_SDK_VERSION := system_current -LOCAL_MIN_SDK_VERSION := 23 # M - -LOCAL_USE_AAPT2 := true - -LOCAL_RESOURCE_DIR := \ - $(LOCAL_PATH)/res \ - -LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-annotations \ - lib-exoplayer \ - lib-exoplayer-v2-core \ - jsr330 \ - -LOCAL_STATIC_ANDROID_LIBRARIES := \ - android-support-compat \ - android-support-core-ui \ - android-support-tv-provider \ - android-support-v4 \ - android-support-v7-appcompat \ - android-support-v7-palette \ - android-support-v7-preference \ - android-support-v7-recyclerview \ - android-support-v14-preference \ - android-support-v17-leanback \ - android-support-v17-preference-leanback \ - live-channels-partner-support \ - live-tv-tuner \ - tv-common \ - - -LOCAL_JAVACFLAGS := -Xlint:deprecation -Xlint:unchecked - -LOCAL_AAPT_FLAGS += \ - --version-name "$(version_name_package)" \ - --version-code $(version_code_package) \ - -LOCAL_JNI_SHARED_LIBRARIES := libtunertvinput_jni -LOCAL_AAPT_FLAGS += --extra-packages com.android.tv.tuner - -include $(BUILD_PACKAGE) - -############################################################# -# Pre-built dependency jars -############################################################# -prebuilts := \ - lib-exoplayer:libs/exoplayer-r1.5.16.aar \ - lib-exoplayer-v2-core:libs/exoplayer-core-2-SNAPHOT-20180114.aar \ - auto-value-jar:../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar \ - javax-annotations-jar:../../../prebuilts/tools/common/m2/repository/javax/annotation/javax.annotation-api/1.2/javax.annotation-api-1.2.jar \ - truth-0-36-prebuilt-jar:../../../prebuilts/tools/common/m2/repository/com/google/truth/truth/0.36/truth-0.36.jar \ - -define define-prebuilt - $(eval tw := $(subst :, ,$(strip $(1)))) \ - $(eval include $(CLEAR_VARS)) \ - $(eval LOCAL_MODULE := $(word 1,$(tw))) \ - $(eval LOCAL_MODULE_TAGS := optional) \ - $(eval LOCAL_MODULE_CLASS := JAVA_LIBRARIES) \ - $(eval LOCAL_SRC_FILES := $(word 2,$(tw))) \ - $(eval LOCAL_UNINSTALLABLE_MODULE := true) \ - $(eval LOCAL_SDK_VERSION := current) \ - $(eval include $(BUILD_PREBUILT)) -endef - -$(foreach p,$(prebuilts),\ - $(call define-prebuilt,$(p))) - -prebuilts := - -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3456f16b..a3988239 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -14,17 +14,19 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +<!-- This manifest is for LiveTv --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.tv" > <uses-sdk android:minSdkVersion="23" - android:targetSdkVersion="26" /> + android:targetSdkVersion="27" /> <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" /> - <uses-permission android:name="android.permission.GLOBAL_SEARCH" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.HDMI_CEC" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS" /> @@ -32,6 +34,7 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_TV_LISTINGS" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <uses-permission android:name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA" /> @@ -70,22 +73,14 @@ <application android:name="com.android.tv.app.LiveTvApplication" android:allowBackup="true" - android:banner="@drawable/banner" - android:icon="@drawable/ic_live_channels" + android:appComponentFactory="android.support.v4.app.CoreComponentFactory" + android:banner="@drawable/live_tv_banner" + android:icon="@drawable/ic_tv_app" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.TV" > - <activity - android:name="com.android.tv.tuner.setup.LiveTvTunerSetupActivity" - android:configChanges="keyboard|keyboardHidden" - android:label="@string/bt_app_name" - android:launchMode="singleInstance" - android:process="com.android.tv.tuner" - android:theme="@style/Theme.Setup.GuidedStep" > - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - </intent-filter> - </activity> + android:theme="@style/Theme.TV" + tools:replace="android:appComponentFactory"> + > <!-- providers are listed here to keep them separate from the internal versions --> <provider @@ -103,7 +98,32 @@ android:exported="false" android:process="com.android.tv.common" /> - <activity android:name="com.android.tv.TvActivity" > + + + <receiver + android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver" + android:exported="true" > + <intent-filter> + <action android:name="android.intent.action.GLOBAL_BUTTON" /> + </intent-filter> + + <!-- + Not directly related to GlobalKeyReceiver but needed to be able to provide our + content rating definitions to the system service. + --> + <intent-filter> + <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" /> + </intent-filter> + + <meta-data + android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS" + android:resource="@xml/tv_content_rating_systems" /> + </receiver> + + <activity + android:name="com.android.tv.TvActivity" + android:exported="true" + android:launchMode="singleTask" > <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -202,8 +222,9 @@ </intent-filter> </activity> <activity - android:name="com.android.tv.dvr.ui.browse.DvrDetailsActivity" + android:name="com.android.tv.ui.DetailsActivity" android:configChanges="keyboard|keyboardHidden" + android:exported="true" android:theme="@style/Theme.TV.Dvr.Browse.Details" /> <activity android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity" @@ -243,6 +264,7 @@ <action android:name="android.intent.action.PACKAGE_ADDED" /> <!-- PACKAGE_CHANGED for package enabled/disabled notification --> <action android:name="android.intent.action.PACKAGE_CHANGED" /> + <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" /> <action android:name="android.intent.action.PACKAGE_REMOVED" /> <data android:scheme="package" /> @@ -255,7 +277,7 @@ android:name="com.android.tv.setup.SystemSetupActivity" android:configChanges="keyboard|keyboardHidden" android:exported="true" - android:label="@string/bt_app_name" + android:label="@string/app_name" android:launchMode="singleInstance" android:theme="@style/Theme.Setup.GuidedStep" > <intent-filter> @@ -263,23 +285,7 @@ <category android:name="android.intent.category.DEFAULT" /> </intent-filter> - </activity> - <!-- - TunerInputController should be the same process with MainActivity to check status - of MainActivity - --> - <receiver - android:name="com.android.tv.tuner.TunerInputController$IntentReceiver" - android:exported="false" > - <intent-filter> - <action android:name="android.intent.action.BOOT_COMPLETED" /> - <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> - <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" /> - <action android:name="com.android.tv.action.APPLICATION_FIRST_LAUNCHED" /> - <action android:name="com.android.tv.action.NETWORK_TUNER_ATTACHED" /> - <action android:name="com.android.tv.action.NETWORK_TUNER_DETACHED" /> - </intent-filter> - </receiver> <!-- DVR --> + </activity> <!-- DVR --> <service android:name="com.android.tv.dvr.recorder.DvrRecordingService" android:label="@string/dvr_service_name" /> @@ -287,48 +293,8 @@ <receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver" /> <service - android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService" - android:exported="false" - android:permission="android.permission.BIND_JOB_SERVICE" - android:process="com.android.tv.tuner" /> - <service android:name="com.android.tv.data.epg.EpgFetchService" android:permission="android.permission.BIND_JOB_SERVICE" /> - - <receiver - android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver" - android:exported="true" > - <intent-filter> - <action android:name="android.intent.action.GLOBAL_BUTTON" /> - </intent-filter> - - <!-- - Not directly related to GlobalKeyReceiver but needed to be able to provide our - content rating definitions to the system service. - --> - <intent-filter> - <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" /> - </intent-filter> - - <meta-data - android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS" - android:resource="@xml/tv_content_rating_systems" /> - </receiver> - - <service - android:name="com.android.tv.tuner.livetuner.LiveTvTunerTvInputService" - android:enabled="false" - android:label="@string/bt_app_name" - android:permission="android.permission.BIND_TV_INPUT" - android:process="com.android.tv.tuner" > - <intent-filter> - <action android:name="android.media.tv.TvInputService" /> - </intent-filter> - - <meta-data - android:name="android.media.tv.input" - android:resource="@xml/ut_tvinputservice" /> - </service> </application> -</manifest>
\ No newline at end of file +</manifest> diff --git a/README.md b/README.md new file mode 100644 index 00000000..63c1f449 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Live TV + +__Live TV__ is the Open Source reference application for watching TV on Android TVs. + +## Source + +The source of truth is an internal google repository (aka google3) at +cs/third_party/java_src/android_app/live_channels + +Changes are made in the google3 repository and automatically pushed here. + +The following files are only in the android repository and must be changed there. + +* *.mk +* \*\*/lib/\*.\* + +## AOSP instructions + +To install LiveTv + +```bash +echo "Compiling" +m -j LiveTv +echo "Installing" +adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk + +``` + +If it is your first time installing LiveTv you will need to do + +```bash +adb root +adb remount +adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk +adb reboot +```
\ No newline at end of file diff --git a/ResourceManifest.xml b/ResourceManifest.xml deleted file mode 100644 index a859327f..00000000 --- a/ResourceManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2018 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" - package="com.android.tv" xmlns:tools="http://schemas.android.com/tools"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> - <application /> -</manifest> diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..23e3dbd1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6' + } +} +apply plugin: 'com.android.application' +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + dexOptions { + preDexLibraries = false + additionalParameters=['--core-library'] + javaMaxHeapSize "6g" + } + android { + defaultConfig { + resConfigs "en" + } + } + defaultConfig { + minSdkVersion 23 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + minifyEnabled false + } + release { + minifyEnabled true + } + } + compileOptions() { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + sourceSets { + main { + res.srcDirs = ['res', 'material_res'] + java.srcDirs = ['src', 'partner_support/src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +repositories { + mavenCentral() + jcenter() + google() +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.palette:palette:1.0.0' + implementation 'androidx.leanback:leanback:1.0.0' + implementation "androidx.tvprovider:tvprovider:1.0.0" + implementation "androidx.recyclerview:recyclerview:1.0.0" + implementation "androidx.recyclerview:recyclerview-selection:1.0.0" + implementation "androidx.palette:palette:1.0.0" + + implementation 'com.google.dagger:dagger:2.18' + implementation 'com.google.dagger:dagger-android:2.18' + annotationProcessor 'com.google.dagger:dagger-compiler:2.18' + annotationProcessor 'com.google.dagger:dagger-android-processor:2.18' + + /*Not building with latest one (1.6.3)*/ + annotationProcessor 'com.google.auto.value:auto-value:1.5.4' + implementation 'com.google.auto.value:auto-value:1.5.4' + implementation 'javax.inject:javax.inject:1' + implementation 'com.google.guava:guava:26.0-android' + implementation project(':common') +}
\ No newline at end of file diff --git a/common/Android.bp b/common/Android.bp new file mode 100644 index 00000000..63759d40 --- /dev/null +++ b/common/Android.bp @@ -0,0 +1,61 @@ +// Copyright (C) 2019 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. + +android_library { + name: "tv-common", + srcs: [ + "src/**/*.java", + "src/**/*.proto", + ], + + sdk_version: "system_current", + + proto: { + type: "lite", + }, + + resource_dirs: ["res"], + + libs: [ + "tv-auto-value-jar", + "tv-auto-factory-jar", + "android-support-annotations", + "tv-error-prone-annotations-jar", + "tv-guava-android-jar", + "jsr330", + "tv-lib-dagger", + "tv-lib-exoplayer", + "tv-lib-exoplayer-v2-core", + "android-support-compat", + "android-support-core-ui", + "android-support-v7-recyclerview", + "android-support-v17-leanback", + ], + + static_libs: ["tv-lib-dagger-android"], + + plugins: [ + "tv-auto-value", + "tv-auto-factory", + "tv-lib-dagger-android-processor", + "tv-lib-dagger-compiler", + ], + + + min_sdk_version: "23", + + // TODO(b/77284273): generate build config after dagger supports libraries + //include $(LOCAL_PATH)/buildconfig.mk + +} diff --git a/common/Android.mk b/common/Android.mk deleted file mode 100644 index 48f969e2..00000000 --- a/common/Android.mk +++ /dev/null @@ -1,31 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# Include all common java files. -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_MODULE := tv-common -LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES -LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := system_current - -LOCAL_USE_AAPT2 := true - -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res - -LOCAL_JAVA_LIBRARIES := \ - android-support-annotations - -LOCAL_DISABLE_RESOLVE_SUPPORT_LIBRARIES := true - -LOCAL_SHARED_ANDROID_LIBRARIES := \ - android-support-compat \ - android-support-core-ui \ - android-support-v7-recyclerview \ - android-support-v17-leanback - -LOCAL_MIN_SDK_VERSION := 23 - -include $(LOCAL_PATH)/buildconfig.mk - -include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/common/AndroidManifest.xml b/common/AndroidManifest.xml index c1c698c5..7002d5fb 100644 --- a/common/AndroidManifest.xml +++ b/common/AndroidManifest.xml @@ -17,6 +17,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.common" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application /> </manifest> diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 00000000..f3714758 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +apply plugin: 'com.android.library' +apply plugin: 'com.google.protobuf' +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6' + } +} +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + dexOptions { + preDexLibraries = false + additionalParameters = ['--core-library'] + javaMaxHeapSize "6g" + } + + android { + defaultConfig { + resConfigs "en" + } + } + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + minifyEnabled false + buildConfigField "boolean", "AOSP", "true" + buildConfigField "boolean", "ENG", "true" + buildConfigField "boolean", "NO_JNI_TEST", "false" + } + release { + minifyEnabled true + buildConfigField "boolean", "AOSP", "true" + buildConfigField "boolean", "ENG", "false" + buildConfigField "boolean", "NO_JNI_TEST", "false" + } + } + compileOptions() { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + proto { + srcDir 'src/com/android/tv/common/compat/internal' + } + } + } +} + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.palette:palette:1.0.0' + implementation 'androidx.leanback:leanback:1.0.0' + implementation "androidx.tvprovider:tvprovider:1.0.0" + implementation "androidx.recyclerview:recyclerview:1.0.0" + implementation "androidx.recyclerview:recyclerview-selection:1.0.0" + implementation "androidx.palette:palette:1.0.0" + implementation 'com.google.guava:guava:26.0-android' + implementation 'com.google.protobuf:protobuf-java:3.0.0' + implementation 'com.google.dagger:dagger:2.18' + implementation 'com.google.dagger:dagger-android:2.18' + annotationProcessor 'com.google.dagger:dagger-compiler:2.18' + annotationProcessor 'com.google.dagger:dagger-android-processor:2.18' +} +protobuf { + // Configure the protoc executable + protoc { + artifact = 'com.google.protobuf:protoc:3.0.0' + + plugins { + javalite { + // The codegen for lite comes as a separate artifact + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + + generateProtoTasks { + all().each { + task -> task.builtins { + remove java + } + task.plugins { + javalite {} + } + } + } + } +}
\ No newline at end of file diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java index 71c9b4d7..45c32567 100644 --- a/common/src/com/android/tv/common/BaseApplication.java +++ b/common/src/com/android/tv/common/BaseApplication.java @@ -17,10 +17,7 @@ package com.android.tv.common; import android.annotation.TargetApi; -import android.app.Application; import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; import android.os.Build; import android.os.StrictMode; import android.support.annotation.VisibleForTesting; @@ -30,9 +27,10 @@ import com.android.tv.common.util.Clock; import com.android.tv.common.util.CommonUtils; import com.android.tv.common.util.Debug; import com.android.tv.common.util.SystemProperties; +import dagger.android.DaggerApplication; /** The base application class for Live TV applications. */ -public abstract class BaseApplication extends Application implements BaseSingletons { +public abstract class BaseApplication extends DaggerApplication implements BaseSingletons { private RecordingStorageStatusManager mRecordingStorageStatusManager; /** @@ -41,7 +39,13 @@ public abstract class BaseApplication extends Application implements BaseSinglet */ @VisibleForTesting public static BaseSingletons sSingletons; - /** Returns the {@link BaseSingletons} using the application context. */ + /** + * Returns the {@link BaseSingletons} using the application context. + * + * @deprecated use {@link com.android.tv.common.singletons.HasSingletons#get(Class, Context)} + * instead + */ + @Deprecated public static BaseSingletons getSingletons(Context context) { // STOP-SHIP: changing the method to protected once the Tuner application is created. // No need to be "synchronized" because this doesn't create any instance. @@ -65,8 +69,15 @@ public abstract class BaseApplication extends Application implements BaseSinglet StrictMode.ThreadPolicy.Builder threadPolicyBuilder = new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog(); // TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed. + // TODO(b/120840665): Restore detecting untagged network sockets StrictMode.VmPolicy.Builder vmPolicyBuilder = - new StrictMode.VmPolicy.Builder().detectAll().penaltyLog(); + new StrictMode.VmPolicy.Builder() + .detectActivityLeaks() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectFileUriExposure() + .detectContentUriWithoutPermission() + .penaltyLog(); if (!CommonUtils.isRunningInTest()) { threadPolicyBuilder.penaltyDialog(); @@ -77,14 +88,6 @@ public abstract class BaseApplication extends Application implements BaseSinglet if (CommonFeatures.DVR.isEnabled(this)) { getRecordingStorageStatusManager(); } - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - // Fetch remote config - getRemoteConfig().fetch(null); - return null; - } - }.execute(); } @Override @@ -101,7 +104,4 @@ public abstract class BaseApplication extends Application implements BaseSinglet } return mRecordingStorageStatusManager; } - - @Override - public abstract Intent getTunerSetupIntent(Context context); } diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java index e735cdb4..10530617 100644 --- a/common/src/com/android/tv/common/BaseSingletons.java +++ b/common/src/com/android/tv/common/BaseSingletons.java @@ -16,20 +16,17 @@ package com.android.tv.common; -import android.content.Context; -import android.content.Intent; -import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig; +import com.android.tv.common.buildtype.HasBuildType; +import com.android.tv.common.flags.has.HasCloudEpgFlags; +import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.util.Clock; /** Injection point for the base app */ -public interface BaseSingletons extends HasRemoteConfig { +public interface BaseSingletons + extends HasCloudEpgFlags, HasBuildType, HasConcurrentDvrPlaybackFlags { Clock getClock(); RecordingStorageStatusManager getRecordingStorageStatusManager(); - - Intent getTunerSetupIntent(Context context); - - String getEmbeddedTunerInputId(); } diff --git a/common/src/com/android/tv/common/BuildConfig.java b/common/src/com/android/tv/common/BuildConfig.java new file mode 100644 index 00000000..b3ad002b --- /dev/null +++ b/common/src/com/android/tv/common/BuildConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 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.common; + +/* Hard coded BuildConfig. */ +public final class BuildConfig { + public static final boolean DEBUG = true; + public static final boolean ENG = false; + public static final boolean NO_JNI_TEST = false; + public static final boolean AOSP = true; + + private BuildConfig() {} +}
\ No newline at end of file diff --git a/common/src/com/android/tv/common/CommonConstants.java b/common/src/com/android/tv/common/CommonConstants.java index ac379d18..43d9851b 100644 --- a/common/src/com/android/tv/common/CommonConstants.java +++ b/common/src/com/android/tv/common/CommonConstants.java @@ -19,10 +19,20 @@ package com.android.tv.common; /** Constants for common use in apps and tests. */ public final class CommonConstants { + @Deprecated // TODO(b/121158110) refactor so this is not needed. public static final String BASE_PACKAGE = +// AOSP_Comment_Out !BuildConfig.AOSP +// AOSP_Comment_Out ? "com.google.android.tv" +// AOSP_Comment_Out : "com.android.tv"; /** A constant for the key of the extra data for the app linking intent. */ public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri"; + /** + * Video is unavailable because the source is not physically connected, for example the HDMI + * cable is not connected. + */ + public static final int VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = 5; + private CommonConstants() {} } diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java index cfdb8e4d..f2fda69c 100644 --- a/common/src/com/android/tv/common/TvContentRatingCache.java +++ b/common/src/com/android/tv/common/TvContentRatingCache.java @@ -23,9 +23,8 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import com.android.tv.common.memory.MemoryManageable; -import java.util.ArrayList; +import com.google.common.collect.ImmutableList; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; @@ -42,19 +41,19 @@ public final class TvContentRatingCache implements MemoryManageable { } // @GuardedBy("TvContentRatingCache.this") - private final Map<String, TvContentRating[]> mRatingsMultiMap = new ArrayMap<>(); + private final Map<String, ImmutableList<TvContentRating>> mRatingsMultiMap = new ArrayMap<>(); /** * Returns an array TvContentRatings from a string of comma separated set of rating strings - * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. Returns - * {@code null} if the string is empty or contains no valid ratings. + * creating each from {@link TvContentRating#unflattenFromString(String)} if needed or an empty + * list if the string is empty or contains no valid ratings. */ - @Nullable - public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) { + public synchronized ImmutableList<TvContentRating> getRatings( + @Nullable String commaSeparatedRatings) { if (TextUtils.isEmpty(commaSeparatedRatings)) { - return null; + return ImmutableList.of(); } - TvContentRating[] tvContentRatings; + ImmutableList<TvContentRating> tvContentRatings; if (mRatingsMultiMap.containsKey(commaSeparatedRatings)) { tvContentRatings = mRatingsMultiMap.get(commaSeparatedRatings); } else { @@ -76,12 +75,13 @@ public final class TvContentRatingCache implements MemoryManageable { /** Returns a sorted array of TvContentRatings from a comma separated string of ratings. */ @VisibleForTesting - static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) { + static ImmutableList<TvContentRating> stringToContentRatings( + @Nullable String commaSeparatedRatings) { if (TextUtils.isEmpty(commaSeparatedRatings)) { - return null; + return ImmutableList.of(); } Set<String> ratingStrings = getSortedSetFromCsv(commaSeparatedRatings); - List<TvContentRating> contentRatings = new ArrayList<>(); + ImmutableList.Builder<TvContentRating> contentRatings = ImmutableList.builder(); for (String rating : ratingStrings) { try { contentRatings.add(TvContentRating.unflattenFromString(rating)); @@ -89,9 +89,7 @@ public final class TvContentRatingCache implements MemoryManageable { Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e); } } - return contentRatings.size() == 0 - ? null - : contentRatings.toArray(new TvContentRating[contentRatings.size()]); + return contentRatings.build(); } private static Set<String> getSortedSetFromCsv(String commaSeparatedRatings) { @@ -118,19 +116,17 @@ public final class TvContentRatingCache implements MemoryManageable { * Returns a string of each flattened content rating, sorted and concatenated together with a * comma. */ - public static String contentRatingsToString(TvContentRating[] contentRatings) { - if (contentRatings == null || contentRatings.length == 0) { + @Nullable + public static String contentRatingsToString( + @Nullable ImmutableList<TvContentRating> contentRatings) { + if (contentRatings == null) { return null; } - String[] ratingStrings = new String[contentRatings.length]; - for (int i = 0; i < contentRatings.length; i++) { - ratingStrings[i] = contentRatings[i].flattenToString(); - } - if (ratingStrings.length == 1) { - return ratingStrings[0]; - } else { - return TextUtils.join(",", toSortedSet(ratingStrings)); + SortedSet<String> ratingStrings = new TreeSet<>(); + for (TvContentRating rating : contentRatings) { + ratingStrings.add(rating.flattenToString()); } + return TextUtils.join(",", ratingStrings); } @Override diff --git a/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java new file mode 100644 index 00000000..8d39b3a6 --- /dev/null +++ b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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.common.buildtype; + +/** {@code AOSP} {@link HasBuildType}. */ +public class AospBuildTypeProvider implements HasBuildType { + + @Override + public BuildType getBuildType() { + return BuildType.AOSP; + } +} diff --git a/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java new file mode 100644 index 00000000..5f18794c --- /dev/null +++ b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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.common.buildtype; + +/** {@code ENG} {@link HasBuildType}. */ +public class EngBuildTypeProvider implements HasBuildType { + + @Override + public BuildType getBuildType() { + return BuildType.ENG; + } +} diff --git a/common/src/com/android/tv/common/buildtype/HasBuildType.java b/common/src/com/android/tv/common/buildtype/HasBuildType.java new file mode 100644 index 00000000..7d5677c9 --- /dev/null +++ b/common/src/com/android/tv/common/buildtype/HasBuildType.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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.common.buildtype; + +/** + * The provides a {@link BuildType} for selecting features in code. + * + * <p>This is considered an anti-pattern and new usages should be discouraged. + */ +public interface HasBuildType { + + /** Compile time constant for build type. */ + enum BuildType { + AOSP, + ENG, + NO_JNI_TEST, + PROD + } + + BuildType getBuildType(); +} diff --git a/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java new file mode 100644 index 00000000..1620af20 --- /dev/null +++ b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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.common.buildtype; + +/** {@code NO_JNI_TEST} {@link HasBuildType}. */ +public class NoJniTestBuildTypeProvider implements HasBuildType { + + @Override + public BuildType getBuildType() { + return BuildType.NO_JNI_TEST; + } +} diff --git a/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java new file mode 100644 index 00000000..16db3263 --- /dev/null +++ b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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.common.buildtype; + +/** {@code Prod} {@link HasBuildType}. */ +public class ProdBuildTypeProvider implements HasBuildType { + + @Override + public BuildType getBuildType() { + return BuildType.PROD; + } +} diff --git a/common/src/com/android/tv/common/compat/README.md b/common/src/com/android/tv/common/compat/README.md new file mode 100644 index 00000000..8d87d83c --- /dev/null +++ b/common/src/com/android/tv/common/compat/README.md @@ -0,0 +1,7 @@ +# TIF Compatibility Library + +This is the temporary location for the of Compatibility Library while it is under development. +It will eventually move to a support library location. + + +See go/tif-compat-proposal
\ No newline at end of file diff --git a/common/src/com/android/tv/common/compat/RecordingSessionCompat.java b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java new file mode 100644 index 00000000..6941e47b --- /dev/null +++ b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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.common.compat; + +import android.content.Context; +import android.media.tv.TvInputService.RecordingSession; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import com.android.tv.common.compat.api.RecordingSessionCompatCommands; +import com.android.tv.common.compat.api.RecordingSessionCompatEvents; +import com.android.tv.common.compat.api.SessionEventNotifier; +import com.android.tv.common.compat.internal.RecordingSessionCompatProcessor; + +/** + * TIF Compatibility for {@link RecordingSession}. + * + * <p>Extends {@code RecordingSession} in a backwards compatible way. + */ +@RequiresApi(api = VERSION_CODES.N) +public abstract class RecordingSessionCompat extends RecordingSession + implements SessionEventNotifier, + RecordingSessionCompatCommands, + RecordingSessionCompatEvents { + + private final RecordingSessionCompatProcessor mProcessor; + + public RecordingSessionCompat(Context context) { + super(context); + mProcessor = new RecordingSessionCompatProcessor(this, this); + } + + @Override + public void onAppPrivateCommand(String action, Bundle data) { + if (!mProcessor.handleAppPrivateCommand(action, data)) { + super.onAppPrivateCommand(action, data); + } + } + + /** Display a debug message to the session for display on dev builds only */ + @Override + public void onDevMessage(String message) {} + + /** Notify the client to Display a message in the application as a toast on dev builds only. */ + @Override + public void notifyDevToast(String message) { + mProcessor.notifyDevToast(message); + } + + /** Notify the client Recording started. */ + @Override + public void notifyRecordingStarted(String uri) { + mProcessor.notifyRecordingStarted(uri); + } +} diff --git a/common/src/com/android/tv/common/compat/TisSessionCompat.java b/common/src/com/android/tv/common/compat/TisSessionCompat.java new file mode 100644 index 00000000..97f4fb32 --- /dev/null +++ b/common/src/com/android/tv/common/compat/TisSessionCompat.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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.common.compat; + +import android.content.Context; +import android.media.tv.TvInputService.Session; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import com.android.tv.common.compat.api.SessionCompatCommands; +import com.android.tv.common.compat.api.SessionCompatEvents; +import com.android.tv.common.compat.api.SessionEventNotifier; +import com.android.tv.common.compat.internal.TifSessionCompatProcessor; + +/** + * TIF Compatibility for {@link Session}. + * + * <p>Extends {@code Session} in a backwards compatible way. + */ +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public abstract class TisSessionCompat extends Session + implements SessionEventNotifier, SessionCompatCommands, SessionCompatEvents { + + private final TifSessionCompatProcessor mTifCompatProcessor; + + public TisSessionCompat(Context context) { + super(context); + mTifCompatProcessor = new TifSessionCompatProcessor(this, this); + } + + @Override + public void onAppPrivateCommand(String action, Bundle data) { + if (!mTifCompatProcessor.handleAppPrivateCommand(action, data)) { + super.onAppPrivateCommand(action, data); + } + } + + @Override + public void onDevMessage(String message) {} + + @Override + public void notifyDevToast(String message) { + mTifCompatProcessor.notifyDevToast(message); + } + + /** + * Notify the application with current signal strength. + * + * <p>At each {MainActivity#tune(boolean)}, the signal strength is implicitly reset to {@link + * TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}. If a TV input supports reporting signal + * strength, it should set the signal strength to {@link + * TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN} in + * {TunerSessionWorker#prepareTune(TunerChannel, String)}, until a valid strength is available. + * + * @param value The current signal strength. Valid values are {@link + * TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}, {@link + * TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN}, and 0 - 100 inclusive. + */ + @Override + public void notifySignalStrength(int value) { + mTifCompatProcessor.notifySignalStrength(value); + } +} diff --git a/common/src/com/android/tv/common/compat/TvInputConstantCompat.java b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java new file mode 100644 index 00000000..251e8481 --- /dev/null +++ b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.common.compat; + +/** Temp TIF Compatibility for {@link TvInputManager} constants. */ +public class TvInputConstantCompat { + + /** + * Status for {@link TisSessionCompat#notifySignalStrength(int)} and + * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}: + * + * <p>SIGNAL_STRENGTH_NOT_USED means the TV Input does not report signal strength. Each onTune + * command implicitly resets the TV App's signal strength state to SIGNAL_STRENGTH_NOT_USED. + */ + public static final int SIGNAL_STRENGTH_NOT_USED = -3; + + /** + * Status for {@link TisSessionCompat#notifySignalStrength(int)} and + * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}: + * + * <p>SIGNAL_STRENGTH_ERROR means exception/error when handling signal strength. + */ + public static final int SIGNAL_STRENGTH_ERROR = -2; + + /** + * Status for {@link TisSessionCompat#notifySignalStrength(int)} and + * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}: + * + * <p>SIGNAL_STRENGTH_UNKNOWN means the TV Input supports signal strength, but does not + * currently know what the strength is. + */ + public static final int SIGNAL_STRENGTH_UNKNOWN = -1; +} diff --git a/common/src/com/android/tv/common/compat/TvInputInfoCompat.java b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java new file mode 100644 index 00000000..685a3ed9 --- /dev/null +++ b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2018 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.common.compat; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputService; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.xmlpull.v1.XmlPullParser; + +/** + * TIF Compatibility for {@link TvInputInfo}. + */ +public class TvInputInfoCompat { + private static final String TAG = "TvInputInfoCompat"; + private static final String ATTRIBUTE_NAMESPACE_ANDROID = + "http://schemas.android.com/apk/res/android"; + private static final String TV_INPUT_XML_START_TAG_NAME = "tv-input"; + private static final String TV_INPUT_EXTRA_XML_START_TAG_NAME = "extra"; + private static final String ATTRIBUTE_NAME = "name"; + private static final String ATTRIBUTE_VALUE = "value"; + private static final String ATTRIBUTE_NAME_AUDIO_ONLY = + "com.android.tv.common.compat.tvinputinfocompat.audioOnly"; + + private final Context mContext; + private final TvInputInfo mTvInputInfo; + private final boolean mAudioOnly; + + public TvInputInfoCompat(Context context, TvInputInfo tvInputInfo) { + mContext = context; + mTvInputInfo = tvInputInfo; + // TODO(b/112938832): use tvInputInfo.isAudioOnly() when SDK is updated + mAudioOnly = Boolean.parseBoolean(getExtras().get(ATTRIBUTE_NAME_AUDIO_ONLY)); + } + + public TvInputInfo getTvInputInfo() { + return mTvInputInfo; + } + + public boolean isAudioOnly() { + return mAudioOnly; + } + + public int getType() { + return mTvInputInfo.getType(); + } + + @VisibleForTesting + public Map<String, String> getExtras() { + ServiceInfo si = mTvInputInfo.getServiceInfo(); + + try { + XmlPullParser parser = getXmlResourceParser(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + if (!TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) { + Log.w(TAG, "Meta-data does not start with " + TV_INPUT_XML_START_TAG_NAME + + " tag for " + si.name); + return Collections.emptyMap(); + } + // <tv-input> start tag found + Map<String, String> extras = new HashMap<>(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.END_TAG + && TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) { + // </tv-input> end tag found + return extras; + } + if (type == XmlPullParser.START_TAG + && TV_INPUT_EXTRA_XML_START_TAG_NAME.equals(parser.getName())) { + String extraName = + parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_NAME); + String extraValue = + parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_VALUE); + if (extraName != null && extraValue != null) { + extras.put(extraName, extraValue); + } + } + } + + } catch (Exception e) { + Log.e(TAG, "Failed to get extras of " + mTvInputInfo.getId() , e); + } + return Collections.emptyMap(); + } + + @VisibleForTesting + XmlPullParser getXmlResourceParser() { + ServiceInfo si = mTvInputInfo.getServiceInfo(); + PackageManager pm = mContext.getPackageManager(); + return si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA); + } +} diff --git a/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java new file mode 100644 index 00000000..143ff25c --- /dev/null +++ b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 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.common.compat; + +import android.content.Context; +import android.media.tv.TvRecordingClient; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.RequiresApi; +import android.util.ArrayMap; +import com.android.tv.common.compat.api.PrivateCommandSender; +import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents; +import com.android.tv.common.compat.api.TvRecordingClientCompatCommands; +import com.android.tv.common.compat.internal.RecordingClientCompatProcessor; + +/** + * TIF Compatibility for {@link TvRecordingClient}. + * + * <p>Extends {@code TvRecordingClient} in a backwards compatible way. + */ +@RequiresApi(api = VERSION_CODES.N) +public class TvRecordingClientCompat extends TvRecordingClient + implements TvRecordingClientCompatCommands, PrivateCommandSender { + + private final RecordingClientCompatProcessor mProcessor; + + /** + * Creates a new TvRecordingClient object. + * + * @param context The application context to create a TvRecordingClient with. + * @param tag A short name for debugging purposes. + * @param callback The callback to receive recording status changes. + * @param handler The handler to invoke the callback on. + */ + public TvRecordingClientCompat( + Context context, String tag, RecordingCallback callback, Handler handler) { + super(context, tag, callback, handler); + RecordingCallbackCompat compatEvents = + callback instanceof RecordingCallbackCompat + ? (RecordingCallbackCompat) callback + : null; + mProcessor = new RecordingClientCompatProcessor(this, compatEvents); + if (compatEvents != null) { + compatEvents.mClientCompatProcessor = mProcessor; + } + } + + /** Tell the session to Display a debug message dev builds only. */ + @Override + public void devMessage(String message) { + mProcessor.devMessage(message); + } + + /** + * TIF Compatibility for {@link RecordingCallback}. + * + * <p>Extends {@code RecordingCallback} in a backwards compatible way. + */ + public static class RecordingCallbackCompat extends RecordingCallback + implements RecordingClientCallbackCompatEvents { + private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>(); + private RecordingClientCompatProcessor mClientCompatProcessor; + + @Override + public void onEvent(String inputId, String eventType, Bundle eventArgs) { + if (mClientCompatProcessor != null + && !mClientCompatProcessor.handleEvent(inputId, eventType, eventArgs)) { + super.onEvent(inputId, eventType, eventArgs); + } + } + + public int getTifCompatVersionForInput(String inputId) { + return inputCompatVersionMap.containsKey(inputId) + ? inputCompatVersionMap.get(inputId) + : 0; + } + + /** Display a message as a toast on dev builds only. */ + @Override + public void onDevToast(String inputId, String message) {} + + /** Recording started. */ + @Override + public void onRecordingStarted(String inputId, String recUri) {} + } +} diff --git a/common/src/com/android/tv/common/compat/TvViewCompat.java b/common/src/com/android/tv/common/compat/TvViewCompat.java new file mode 100644 index 00000000..f44564d3 --- /dev/null +++ b/common/src/com/android/tv/common/compat/TvViewCompat.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 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.common.compat; + +import android.content.Context; +import android.media.tv.TvView; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import android.util.ArrayMap; +import android.util.AttributeSet; +import com.android.tv.common.compat.api.PrivateCommandSender; +import com.android.tv.common.compat.api.TvInputCallbackCompatEvents; +import com.android.tv.common.compat.api.TvViewCompatCommands; +import com.android.tv.common.compat.internal.TvViewCompatProcessor; + +/** + * TIF Compatibility for {@link TvView}. + * + * <p>Extends {@code TvView} in a backwards compatible way. + */ +@RequiresApi(api = VERSION_CODES.LOLLIPOP) +public class TvViewCompat extends TvView implements TvViewCompatCommands, PrivateCommandSender { + + private final TvViewCompatProcessor mTvViewCompatProcessor; + + public TvViewCompat(Context context) { + this(context, null); + } + + public TvViewCompat(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvViewCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mTvViewCompatProcessor = new TvViewCompatProcessor(this); + } + + @Override + public void setCallback(TvInputCallback callback) { + super.setCallback(callback); + if (callback instanceof TvInputCallbackCompat) { + TvInputCallbackCompat compatEvents = (TvInputCallbackCompat) callback; + mTvViewCompatProcessor.setCallback(compatEvents); + compatEvents.mTvViewCompatProcessor = mTvViewCompatProcessor; + } + } + + @Override + public void devMessage(String message) { + mTvViewCompatProcessor.devMessage(message); + } + + /** + * TIF Compatibility for {@link TvInputCallback}. + * + * <p>Extends {@code TvInputCallback} in a backwards compatible way. + */ + public static class TvInputCallbackCompat extends TvInputCallback + implements TvInputCallbackCompatEvents { + private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>(); + private TvViewCompatProcessor mTvViewCompatProcessor; + + @Override + public void onEvent(String inputId, String eventType, Bundle eventArgs) { + if (mTvViewCompatProcessor != null + && !mTvViewCompatProcessor.handleEvent(inputId, eventType, eventArgs)) { + super.onEvent(inputId, eventType, eventArgs); + } + } + + public int getTifCompatVersionForInput(String inputId) { + return inputCompatVersionMap.containsKey(inputId) + ? inputCompatVersionMap.get(inputId) + : 0; + } + + @Override + public void onDevToast(String inputId, String message) {} + + /** + * This is called when the signal strength is notified. + * + * @param inputId The ID of the TV input bound to this view. + * @param value The current signal strength. Should be one of the followings. + * <ul> + * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED} + * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR} + * <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN} + * <li>{int [0, 100]} + * </ul> + */ + @Override + public void onSignalStrength(String inputId, int value) {} + } +} diff --git a/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java new file mode 100644 index 00000000..11de970f --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +import android.os.Bundle; + +/** Sends a command from the TV App to a {@link android.media.tv.TvInputService.Session} */ +public interface PrivateCommandSender { + + void sendAppPrivateCommand(String action, Bundle data); +} diff --git a/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java new file mode 100644 index 00000000..753703c4 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** + * {@link android.media.tv.TvRecordingClient} implements this to receive notification from a {@link + * android.media.tv.TvInputService.RecordingSession} + */ +public interface RecordingClientCallbackCompatEvents { + /** Display a message in the application as a toast on dev builds only */ + void onDevToast(String inputId, String message); + + void onRecordingStarted(String inputId, String recUri); +} diff --git a/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java new file mode 100644 index 00000000..9deaa41f --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */ +public interface RecordingSessionCompatCommands { + + void onDevMessage(String message); +} diff --git a/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java new file mode 100644 index 00000000..812bba62 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Events sent from the {@link android.media.tv.TvInputService.RecordingSession} to the TV App. */ +public interface RecordingSessionCompatEvents { + + void notifyDevToast(String message); + + void notifyRecordingStarted(String value); +} diff --git a/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java new file mode 100644 index 00000000..bef4ad27 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */ +public interface SessionCompatCommands { + + void onDevMessage(String message); +} diff --git a/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java new file mode 100644 index 00000000..a3af8f3c --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Events sent from the {@link android.media.tv.TvInputService.Session} to the TV App. */ +public interface SessionCompatEvents { + + void notifyDevToast(String message); + + void notifySignalStrength(int value); +} diff --git a/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java new file mode 100644 index 00000000..66c5c3a8 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +import android.os.Bundle; + +/** Sends events from a {@link android.media.tv.TvInputService.Session} to the TV App. */ +public interface SessionEventNotifier { + + void notifySessionEvent(String event, Bundle data); +} diff --git a/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java new file mode 100644 index 00000000..e6b241b7 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** + * {@link android.media.tv.TvView.TvInputCallback} implements this to receive notification from a + * {@link android.media.tv.TvInputService.Session} + */ +public interface TvInputCallbackCompatEvents { + void onDevToast(String inputId, String message); + + void onSignalStrength(String inputId, int value); +} diff --git a/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java new file mode 100644 index 00000000..c1852165 --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */ +public interface TvRecordingClientCompatCommands { + + void devMessage(String message); +} diff --git a/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java new file mode 100644 index 00000000..5abc6bce --- /dev/null +++ b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 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.common.compat.api; + +/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */ +public interface TvViewCompatCommands { + + void devMessage(String message); +} diff --git a/common/src/com/android/tv/common/compat/internal/Constants.java b/common/src/com/android/tv/common/compat/internal/Constants.java new file mode 100644 index 00000000..993822c1 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/Constants.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; +/** Static constants use by the TIF compat library */ +final class Constants { + static final String ACTION_GET_VERSION = "com.android.tv.common.compat.action.GET_VERSION"; + static final String EVENT_GET_VERSION = "com.android.tv.common.compat.event.GET_VERSION"; + static final String ACTION_COMPAT_ON = "com.android.tv.common.compat.action.COMPAT_ON"; + static final String EVENT_COMPAT_NOTIFY = "com.android.tv.common.compat.event.COMPAT_NOTIFY"; + static final String EVENT_COMPAT_NOTIFY_ERROR = + "com.android.tv.common.compat.event.COMPAT_NOTIFY_ERROR"; + static final int TIF_COMPAT_VERSION = 1; + + private Constants() {} +} diff --git a/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java new file mode 100644 index 00000000..f83228c8 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.support.annotation.Nullable; +import android.util.Log; +import com.android.tv.common.compat.api.PrivateCommandSender; +import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents; +import com.android.tv.common.compat.api.TvViewCompatCommands; +import com.android.tv.common.compat.internal.Commands.OnDevMessage; +import com.android.tv.common.compat.internal.Commands.PrivateCommand; +import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast; +import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent; + +/** + * Sends {@link RecordingCommands} to the {@link android.media.tv.TvInputService.RecordingSession} + * via {@link PrivateCommandSender} and receives notification events from the session forwarding + * them to {@link RecordingClientCallbackCompatEvents} + */ +public final class RecordingClientCompatProcessor + extends ViewCompatProcessor<PrivateCommand, RecordingSessionEvent> + implements TvViewCompatCommands { + private static final String TAG = "RecordingClientCompatProcessor"; + + @Nullable private final RecordingClientCallbackCompatEvents mCallback; + + public RecordingClientCompatProcessor( + PrivateCommandSender commandSender, + @Nullable RecordingClientCallbackCompatEvents callback) { + super(commandSender, RecordingSessionEvent.parser()); + mCallback = callback; + } + + @Override + public void devMessage(String message) { + OnDevMessage devMessage = OnDevMessage.newBuilder().setMessage(message).build(); + PrivateCommand privateCommand = + createPrivateCommandCommand().setOnDevMessage(devMessage).build(); + sendCompatCommand(privateCommand); + } + + private PrivateCommand.Builder createPrivateCommandCommand() { + return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION); + } + + @Override + protected final void handleSessionEvent(String inputId, RecordingSessionEvent sessionEvent) { + switch (sessionEvent.getEventCase()) { + case NOTIFY_DEV_MESSAGE: + handle(inputId, sessionEvent.getNotifyDevMessage()); + break; + case RECORDING_STARTED: + handle(inputId, sessionEvent.getRecordingStarted()); + break; + + case EVENT_NOT_SET: + Log.w(TAG, "Error event not set compat notify "); + } + } + + private void handle(String inputId, NotifyDevToast devToast) { + if (devToast != null && mCallback != null) { + mCallback.onDevToast(inputId, devToast.getMessage()); + } + } + + private void handle(String inputId, RecordingEvents.RecordingStarted recStart) { + if (recStart != null && mCallback != null) { + mCallback.onRecordingStarted(inputId, recStart.getUri()); + } + } +} diff --git a/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java new file mode 100644 index 00000000..84ec5503 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.util.Log; +import com.android.tv.common.compat.api.RecordingSessionCompatCommands; +import com.android.tv.common.compat.api.RecordingSessionCompatEvents; +import com.android.tv.common.compat.api.SessionEventNotifier; +import com.android.tv.common.compat.internal.RecordingCommands.PrivateRecordingCommand; +import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast; +import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent; +import com.android.tv.common.compat.internal.RecordingEvents.RecordingStarted; + +/** + * Sends {@link RecordingSessionCompatEvents} to the TV App via {@link SessionEventNotifier} and + * receives Commands from TV App forwarding them to {@link RecordingSessionCompatProcessor} + */ +public final class RecordingSessionCompatProcessor + extends SessionCompatProcessor<PrivateRecordingCommand, RecordingSessionEvent> + implements RecordingSessionCompatEvents { + + private static final String TAG = "RecordingSessionCompatProc"; + + private final RecordingSessionCompatCommands mRecordingSessionOnCompat; + + public RecordingSessionCompatProcessor( + SessionEventNotifier sessionEventNotifier, + RecordingSessionCompatCommands recordingSessionOnCompat) { + super(sessionEventNotifier, PrivateRecordingCommand.parser()); + mRecordingSessionOnCompat = recordingSessionOnCompat; + } + + @Override + protected void onCompat(PrivateRecordingCommand privateCommand) { + switch (privateCommand.getCommandCase()) { + case ON_DEV_MESSAGE: + mRecordingSessionOnCompat.onDevMessage( + privateCommand.getOnDevMessage().getMessage()); + break; + case COMMAND_NOT_SET: + Log.w(TAG, "Command not set "); + } + } + + @Override + public void notifyDevToast(String message) { + NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build(); + RecordingSessionEvent sessionEvent = + createSessionEvent().setNotifyDevMessage(devMessage).build(); + notifyCompat(sessionEvent); + } + + @Override + public void notifyRecordingStarted(String uri) { + RecordingStarted event = RecordingStarted.newBuilder().setUri(uri).build(); + RecordingSessionEvent sessionEvent = + createSessionEvent().setRecordingStarted(event).build(); + notifyCompat(sessionEvent); + } + + private RecordingSessionEvent.Builder createSessionEvent() { + + return RecordingSessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION); + } +} diff --git a/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java new file mode 100644 index 00000000..7f27a243 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.os.Bundle; +import android.util.Log; +import com.android.tv.common.compat.api.SessionEventNotifier; +import com.google.protobuf.GeneratedMessageLite; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Parser; + +/** + * Sends {@code events} to the TV App via {@link SessionEventNotifier} and receives {@code commands} + * from TV App. + */ +abstract class SessionCompatProcessor< + C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> { + private static final String TAG = "SessionCompatProcessor"; + private final SessionEventNotifier mSessionEventNotifier; + private final Parser<C> mCommandParser; + + SessionCompatProcessor(SessionEventNotifier sessionEventNotifier, Parser<C> commandParser) { + mSessionEventNotifier = sessionEventNotifier; + mCommandParser = commandParser; + } + + public final boolean handleAppPrivateCommand(String action, Bundle data) { + switch (action) { + case Constants.ACTION_GET_VERSION: + Bundle response = new Bundle(); + response.putInt(Constants.EVENT_GET_VERSION, Constants.TIF_COMPAT_VERSION); + mSessionEventNotifier.notifySessionEvent(Constants.EVENT_GET_VERSION, response); + return true; + case Constants.ACTION_COMPAT_ON: + byte[] bytes = data.getByteArray(Constants.ACTION_COMPAT_ON); + try { + C privateCommand = mCommandParser.parseFrom(bytes); + onCompat(privateCommand); + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Error parsing compat data", e); + } + + return true; + default: + return false; + } + } + + abstract void onCompat(C privateCommand); + + final void notifyCompat(E event) { + Bundle response = new Bundle(); + try { + byte[] bytes = event.toByteArray(); + response.putByteArray(Constants.EVENT_COMPAT_NOTIFY, bytes); + } catch (Exception e) { + Log.w(TAG, "Failed to send " + event, e); + response.putString(Constants.EVENT_COMPAT_NOTIFY_ERROR, e.getMessage()); + } + mSessionEventNotifier.notifySessionEvent(Constants.EVENT_COMPAT_NOTIFY, response); + } +} diff --git a/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java new file mode 100644 index 00000000..dd7a3b3e --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.util.Log; +import com.android.tv.common.compat.api.SessionCompatCommands; +import com.android.tv.common.compat.api.SessionCompatEvents; +import com.android.tv.common.compat.api.SessionEventNotifier; +import com.android.tv.common.compat.internal.Commands.PrivateCommand; +import com.android.tv.common.compat.internal.Events.NotifyDevToast; +import com.android.tv.common.compat.internal.Events.NotifySignalStrength; +import com.android.tv.common.compat.internal.Events.SessionEvent; + +/** + * Sends {@link SessionCompatEvents} to the TV App via {@link SessionEventNotifier} and receives + * Commands from TV App forwarding them to {@link SessionCompatCommands} + */ +public final class TifSessionCompatProcessor + extends SessionCompatProcessor<PrivateCommand, SessionEvent> + implements SessionCompatEvents { + + private static final String TAG = "TifSessionCompatProcessor"; + + private final SessionCompatCommands mSessionOnCompat; + + public TifSessionCompatProcessor( + SessionEventNotifier sessionEventNotifier, SessionCompatCommands sessionOnCompat) { + super(sessionEventNotifier, PrivateCommand.parser()); + mSessionOnCompat = sessionOnCompat; + } + + @Override + protected void onCompat(Commands.PrivateCommand privateCommand) { + switch (privateCommand.getCommandCase()) { + case ON_DEV_MESSAGE: + mSessionOnCompat.onDevMessage(privateCommand.getOnDevMessage().getMessage()); + break; + case COMMAND_NOT_SET: + Log.w(TAG, "Command not set "); + } + } + + @Override + public void notifyDevToast(String message) { + NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build(); + SessionEvent sessionEvent = createSessionEvent().setNotifyDevMessage(devMessage).build(); + notifyCompat(sessionEvent); + } + + @Override + public void notifySignalStrength(int value) { + NotifySignalStrength signalStrength = + NotifySignalStrength.newBuilder().setSignalStrength(value).build(); + SessionEvent sessionEvent = + createSessionEvent().setNotifySignalStrength(signalStrength).build(); + notifyCompat(sessionEvent); + } + + private SessionEvent.Builder createSessionEvent() { + return SessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION); + } +} diff --git a/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java new file mode 100644 index 00000000..382f8d8a --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.support.annotation.NonNull; +import android.util.Log; +import com.android.tv.common.compat.api.PrivateCommandSender; +import com.android.tv.common.compat.api.TvInputCallbackCompatEvents; +import com.android.tv.common.compat.api.TvViewCompatCommands; +import com.android.tv.common.compat.internal.Commands.OnDevMessage; +import com.android.tv.common.compat.internal.Commands.PrivateCommand; +import com.android.tv.common.compat.internal.Events.NotifyDevToast; +import com.android.tv.common.compat.internal.Events.NotifySignalStrength; +import com.android.tv.common.compat.internal.Events.SessionEvent; + +/** + * Sends {@link TvViewCompatCommands} to the {@link android.media.tv.TvInputService.Session} via + * {@link PrivateCommandSender} and receives notification events from the session forwarding them to + * {@link TvInputCallbackCompatEvents} + */ +public final class TvViewCompatProcessor extends ViewCompatProcessor<PrivateCommand, SessionEvent> + implements TvViewCompatCommands { + private static final String TAG = "TvViewCompatProcessor"; + + private TvInputCallbackCompatEvents mCallback; + + public TvViewCompatProcessor(PrivateCommandSender commandSender) { + super(commandSender, SessionEvent.parser()); + } + + @Override + public void devMessage(String message) { + OnDevMessage devMessage = Commands.OnDevMessage.newBuilder().setMessage(message).build(); + Commands.PrivateCommand privateCommand = + createPrivateCommandCommand().setOnDevMessage(devMessage).build(); + sendCompatCommand(privateCommand); + } + + @NonNull + private PrivateCommand.Builder createPrivateCommandCommand() { + return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION); + } + + public void onDevToast(String inputId, String message) {} + + public void onSignalStrength(String inputId, int value) {} + + @Override + protected final void handleSessionEvent(String inputId, Events.SessionEvent sessionEvent) { + switch (sessionEvent.getEventCase()) { + case NOTIFY_DEV_MESSAGE: + handle(inputId, sessionEvent.getNotifyDevMessage()); + break; + case NOTIFY_SIGNAL_STRENGTH: + handle(inputId, sessionEvent.getNotifySignalStrength()); + break; + case EVENT_NOT_SET: + Log.w(TAG, "Error event not set compat notify "); + } + } + + private void handle(String inputId, NotifyDevToast devToast) { + if (devToast != null && mCallback != null) { + mCallback.onDevToast(inputId, devToast.getMessage()); + } + } + + private void handle(String inputId, NotifySignalStrength signalStrength) { + if (signalStrength != null && mCallback != null) { + mCallback.onSignalStrength(inputId, signalStrength.getSignalStrength()); + } + } + + public void setCallback(TvInputCallbackCompatEvents callback) { + this.mCallback = callback; + } +} diff --git a/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java new file mode 100644 index 00000000..dc6bbfa5 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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.common.compat.internal; + +import android.os.Bundle; +import android.util.ArrayMap; +import android.util.Log; +import com.android.tv.common.compat.api.PrivateCommandSender; +import com.google.protobuf.GeneratedMessageLite; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Parser; + +/** + * Sends {@code commands} to the {@code session} via {@link PrivateCommandSender} and receives + * notification events from the session forwarding them to {@link + * com.android.tv.common.compat.api.TvInputCallbackCompatEvents} + */ +abstract class ViewCompatProcessor< + C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> { + private static final String TAG = "ViewCompatProcessor"; + private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>(); + + private final Parser<E> mEventParser; + private final PrivateCommandSender mCommandSender; + + ViewCompatProcessor(PrivateCommandSender commandSender, Parser<E> eventParser) { + mCommandSender = commandSender; + mEventParser = eventParser; + } + + private final E sessionEventFromBundle(Bundle eventArgs) throws InvalidProtocolBufferException { + + byte[] protoBytes = eventArgs.getByteArray(Constants.EVENT_COMPAT_NOTIFY); + return protoBytes == null || protoBytes.length == 0 + ? null + : mEventParser.parseFrom(protoBytes); + } + + final void sendCompatCommand(C privateCommand) { + try { + Bundle data = new Bundle(); + data.putByteArray(Constants.ACTION_COMPAT_ON, privateCommand.toByteArray()); + mCommandSender.sendAppPrivateCommand(Constants.ACTION_COMPAT_ON, data); + } catch (Exception e) { + Log.w(TAG, "Error sending compat action " + privateCommand, e); + } + } + + public boolean handleEvent(String inputId, String eventType, Bundle eventArgs) { + switch (eventType) { + case Constants.EVENT_GET_VERSION: + int version = eventArgs.getInt(Constants.EVENT_GET_VERSION, 0); + inputCompatVersionMap.put(inputId, version); + return true; + case Constants.EVENT_COMPAT_NOTIFY: + try { + E sessionEvent = sessionEventFromBundle(eventArgs); + if (sessionEvent != null) { + handleSessionEvent(inputId, sessionEvent); + } else { + String errorMessage = + eventArgs.getString(Constants.EVENT_COMPAT_NOTIFY_ERROR); + Log.w(TAG, "Error sent in compat notify " + errorMessage); + } + + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Error parsing in compat notify for " + inputId); + } + + return true; + default: + return false; + } + } + + protected abstract void handleSessionEvent(String inputId, E sessionEvent); +} diff --git a/common/src/com/android/tv/common/compat/internal/recording_commands.proto b/common/src/com/android/tv/common/compat/internal/recording_commands.proto new file mode 100644 index 00000000..ce59bfa0 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/recording_commands.proto @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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. + */ + +// A set of Private Commands to send to a TVInputService.Session, in particular +// support new features on older devices. NOTE: this proto is internal to this +// package and should not be used outside it. + +syntax = "proto3"; +package android.tv.common.compat.internal; + +option java_outer_classname = "RecordingCommands"; +option java_package = "com.android.tv.common.compat.internal"; + +// Wraps messages for sending to a session a private command. +message PrivateRecordingCommand { + uint32 compat_version = 1; + + oneof command { + OnDevMessage on_dev_message = 2; + } +} + +// Display a debug message dev builds only. +message OnDevMessage { + string message = 1; +} diff --git a/common/src/com/android/tv/common/compat/internal/recording_events.proto b/common/src/com/android/tv/common/compat/internal/recording_events.proto new file mode 100644 index 00000000..68db5ddf --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/recording_events.proto @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 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. + */ + +// A set of Session events to send from a TVInputService.Session, in particular +// support new features on older devices. NOTE: this proto is internal to this +// package and should not be used outside it. +syntax = "proto3"; +package android.tv.common.compat.internal; + +option java_outer_classname = "RecordingEvents"; +option java_package = "com.android.tv.common.compat.internal"; + +// Wraps messages for sending from a session as an Event. +// RecordingSessionCompat will have a notify{EventMessageName} for each event +// TvRecordingClientCompat will have a on{EventMessageName} for each event + +message RecordingSessionEvent { + uint32 compat_version = 1; + + oneof event { + NotifyDevToast notify_dev_message = 2; + RecordingStarted recording_started = 3; + } +} + +// Display a message as a toast on dev builds only +message NotifyDevToast { + string message = 1; +} + +// Recording started. +message RecordingStarted { + // Recording URI. + string uri = 1; +} + diff --git a/common/src/com/android/tv/common/compat/internal/tif_commands.proto b/common/src/com/android/tv/common/compat/internal/tif_commands.proto new file mode 100644 index 00000000..d5867703 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/tif_commands.proto @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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. + */ + +// A set of Private Commands to send to a TVInputService.Session, in particular +// support new features on older devices. NOTE: this proto is internal to this +// package and should not be used outside it. + +syntax = "proto3"; +package android.tv.common.compat.internal; + +option java_outer_classname = "Commands"; +option java_package = "com.android.tv.common.compat.internal"; + +// Wraps messages for sending to a session a private command. +message PrivateCommand { + uint32 compat_version = 1; + + oneof command { + OnDevMessage on_dev_message = 2; + } +} + +// Sends a debug message to the session for display on dev builds only +message OnDevMessage { + string message = 1; +} diff --git a/common/src/com/android/tv/common/compat/internal/tif_events.proto b/common/src/com/android/tv/common/compat/internal/tif_events.proto new file mode 100644 index 00000000..6e71ae11 --- /dev/null +++ b/common/src/com/android/tv/common/compat/internal/tif_events.proto @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 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. + */ + +// A set of Session events to send from a TVInputService.Session, in particular +// support new features on older devices. NOTE: this proto is internal to this +// package and should not be used outside it. +syntax = "proto3"; +package android.tv.common.compat.internal; + +option java_outer_classname = "Events"; +option java_package = "com.android.tv.common.compat.internal"; + +// Wraps messages for sending from a session as an Event. +message SessionEvent { + uint32 compat_version = 1; + + oneof event { + NotifyDevToast notify_dev_message = 2; + NotifySignalStrength notify_signal_strength = 3; + } +} + +// Send a message to the application to be displayed as a toast on dev builds +// only +message NotifyDevToast { + string message = 1; +} + +// Notifies the TV Application the current signal strength. +message NotifySignalStrength { + // The signal strength as a percent (0 to 100), + // with -1 meaning unknown, -2 meaning not used. + int32 signal_strength = 1; +} diff --git a/common/src/com/android/tv/common/config/DefaultConfigManager.java b/common/src/com/android/tv/common/config/DefaultConfigManager.java deleted file mode 100644 index ae240855..00000000 --- a/common/src/com/android/tv/common/config/DefaultConfigManager.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2016 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.common.config; - -import android.content.Context; -import com.android.tv.common.config.api.RemoteConfig; - -/** Stub Remote Config. */ -public class DefaultConfigManager { - public static final long DEFAULT_LONG_VALUE = 0; - - public static DefaultConfigManager createInstance(Context context) { - return new DefaultConfigManager(); - } - - private StubRemoteConfig mRemoteConfig = new StubRemoteConfig(); - - public RemoteConfig getRemoteConfig() { - return mRemoteConfig; - } - - private static class StubRemoteConfig implements RemoteConfig { - @Override - public void fetch(OnRemoteConfigUpdatedListener listener) {} - - @Override - public String getString(String key) { - return null; - } - - @Override - public boolean getBoolean(String key) { - return false; - } - - @Override - public long getLong(String key) { - return DEFAULT_LONG_VALUE; - } - - @Override - public long getLong(String key, long defaultValue) { - return defaultValue; - } - } -} diff --git a/common/src/com/android/tv/common/config/RemoteConfigFeature.java b/common/src/com/android/tv/common/config/RemoteConfigFeature.java deleted file mode 100644 index 2ea381f0..00000000 --- a/common/src/com/android/tv/common/config/RemoteConfigFeature.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 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.common.config; - -import android.content.Context; -import com.android.tv.common.BaseApplication; -import com.android.tv.common.feature.Feature; - -/** - * A {@link Feature} controlled by a {@link com.android.tv.common.config.api.RemoteConfig} boolean. - */ -public class RemoteConfigFeature implements Feature { - private final String mKey; - - /** Creates a {@link RemoteConfigFeature for the {@code key}. */ - public static RemoteConfigFeature fromKey(String key) { - return new RemoteConfigFeature(key); - } - - private RemoteConfigFeature(String key) { - mKey = key; - } - - @Override - public boolean isEnabled(Context context) { - return BaseApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey); - } -} diff --git a/common/src/com/android/tv/common/config/api/RemoteConfig.java b/common/src/com/android/tv/common/config/api/RemoteConfig.java deleted file mode 100644 index 74597f9d..00000000 --- a/common/src/com/android/tv/common/config/api/RemoteConfig.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2016 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.common.config.api; - -/** - * Manages Live TV Configuration, allowing remote updates. - * - * <p>This is a thin wrapper around <a - * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a> - */ -public interface RemoteConfig { - - /** Used to inject a remote config */ - interface HasRemoteConfig { - RemoteConfig getRemoteConfig(); - } - - /** Notified on successful completion of a {@link #fetch)} */ - interface OnRemoteConfigUpdatedListener { - void onRemoteConfigUpdated(); - } - - /** Starts a fetch and notifies {@code listener} on successful completion. */ - void fetch(OnRemoteConfigUpdatedListener listener); - - /** Gets value as a string corresponding to the specified key. */ - String getString(String key); - - /** Gets value as a boolean corresponding to the specified key. */ - boolean getBoolean(String key); - - /** Gets value as a long corresponding to the specified key. */ - long getLong(String key); - - /** - * Gets value as a long corresponding to the specified key. Returns the defaultValue if no value - * is found. - */ - long getLong(String key, long defaultValue); -} diff --git a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java deleted file mode 100644 index 6da89fb9..00000000 --- a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2018 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.common.config.api; - -/** Wrapper for a RemoteConfig key and default value. */ -public abstract class RemoteConfigValue<T> { - private final T defaultValue; - private final String key; - - private RemoteConfigValue(String key, T defaultValue) { - this.defaultValue = defaultValue; - this.key = key; - } - - /** Create with the given key and default value */ - public static RemoteConfigValue<Long> create(String key, long defaultValue) { - return new RemoteConfigValue<Long>(key, defaultValue) { - @Override - public Long get(RemoteConfig remoteConfig) { - return remoteConfig.getLong(key, defaultValue); - } - }; - } - - public abstract T get(RemoteConfig remoteConfig); - - public final T getDefaultValue() { - return defaultValue; - } - - @Override - public final String toString() { - return "RemoteConfigValue(key=" + key + ", defalutValue=" + defaultValue + "]"; - } -} diff --git a/common/src/com/android/tv/common/dagger/ApplicationModule.java b/common/src/com/android/tv/common/dagger/ApplicationModule.java new file mode 100644 index 00000000..4655f777 --- /dev/null +++ b/common/src/com/android/tv/common/dagger/ApplicationModule.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 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.common.dagger; + +import android.app.Application; +import android.content.ContentResolver; +import android.content.Context; +import android.os.Looper; +import com.android.tv.common.dagger.annotations.ApplicationContext; +import com.android.tv.common.dagger.annotations.MainLooper; +import dagger.Module; +import dagger.Provides; + +/** + * Provides application-scope qualifiers for the {@link Application}, the application context, and + * the application's main looper. + */ +@Module +public final class ApplicationModule { + private final Application mApplication; + + public ApplicationModule(Application application) { + mApplication = application; + } + + @Provides + Application provideApplication() { + return mApplication; + } + + @Provides + @ApplicationContext + Context provideContext() { + return mApplication.getApplicationContext(); + } + + @Provides + @MainLooper + static Looper provideMainLooper() { + return Looper.getMainLooper(); + } + + @Provides + ContentResolver provideContentResolver() { + return mApplication.getContentResolver(); + } +} diff --git a/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java new file mode 100644 index 00000000..86318156 --- /dev/null +++ b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2019 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.common.dagger.annotations; + +import javax.inject.Qualifier; + +/** Annotation for requesting the application's context. */ +@Qualifier +public @interface ApplicationContext {} diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java index 3e24a496..a8b4100f 100644 --- a/src/com/android/tv/util/Filter.java +++ b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2019 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. @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.tv.common.dagger.annotations; -package com.android.tv.util; +import javax.inject.Qualifier; -/** Interface to decide whether an input is filtered out or not. */ -public interface Filter<T> { - /** Returns true, if {@code input} is acceptable. */ - boolean filter(T input); -} +/** Annotation for requesting a Looper that is on the UI thread. */ +@Qualifier +public @interface MainLooper {} diff --git a/common/src/com/android/tv/common/data/RecordedProgramState.java b/common/src/com/android/tv/common/data/RecordedProgramState.java new file mode 100644 index 00000000..3bfad88d --- /dev/null +++ b/common/src/com/android/tv/common/data/RecordedProgramState.java @@ -0,0 +1,14 @@ +package com.android.tv.common.data; + +/** The recording state. */ +// TODO(b/25023911): Use @SimpleEnum when it is supported by AutoValue +public enum RecordedProgramState { + // TODO(b/71717809): Document each state. + NOT_SET, + STARTED, + FINISHED, + PARTIAL, + FAILED, + DELETE, + DELETED, +} diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java index c9bacac4..b8370ad6 100644 --- a/common/src/com/android/tv/common/experiments/ExperimentFlag.java +++ b/common/src/com/android/tv/common/experiments/ExperimentFlag.java @@ -19,41 +19,63 @@ package com.android.tv.common.experiments; import android.support.annotation.VisibleForTesting; import com.android.tv.common.BuildConfig; +import com.google.common.base.Supplier; /** Experiments return values based on user, device and other criteria. */ public final class ExperimentFlag<T> { + // NOTE: sAllowOverrides IS NEVER USED in the non AOSP version. private static boolean sAllowOverrides = false; @VisibleForTesting public static void initForTest() { + /* Begin_AOSP_Comment_Out + if (!BuildConfig.AOSP) { + PhenotypeFlag.initForTest(); + return; + } + End_AOSP_Comment_Out */ sAllowOverrides = true; } /** Returns a boolean experiment */ public static ExperimentFlag<Boolean> createFlag( +// AOSP_Comment_Out Supplier<Boolean> phenotypeFlag, boolean defaultValue) { return new ExperimentFlag<>( +// AOSP_Comment_Out phenotypeFlag, defaultValue); } private final T mDefaultValue; +// AOSP_Comment_Out private final Supplier<T> mPhenotypeFlag; +// AOSP_Comment_Out // NOTE: mOverrideValue IS NEVER USED in the non AOSP version. private T mOverrideValue = null; + // mOverridden IS NEVER USED in the non AOSP version. private boolean mOverridden = false; private ExperimentFlag( +// AOSP_Comment_Out Supplier<T> phenotypeFlag, + // NOTE: defaultValue IS NEVER USED in the non AOSP version. T defaultValue) { mDefaultValue = defaultValue; +// AOSP_Comment_Out mPhenotypeFlag = phenotypeFlag; } /** Returns value for this experiment */ public T get() { + /* Begin_AOSP_Comment_Out + if (!BuildConfig.AOSP) { + return mPhenotypeFlag.get(); + } + End_AOSP_Comment_Out */ return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue; } @VisibleForTesting public void override(T t) { + if (sAllowOverrides) { mOverridden = true; mOverrideValue = t; @@ -64,4 +86,11 @@ public final class ExperimentFlag<T> { public void resetOverride() { mOverridden = false; } + + /* Begin_AOSP_Comment_Out + @VisibleForTesting + T getAospDefaultValueForTesting() { + return mDefaultValue; + } + End_AOSP_Comment_Out */ } diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java index 96b15e53..9bfdb547 100644 --- a/common/src/com/android/tv/common/experiments/Experiments.java +++ b/common/src/com/android/tv/common/experiments/Experiments.java @@ -19,6 +19,7 @@ package com.android.tv.common.experiments; import static com.android.tv.common.experiments.ExperimentFlag.createFlag; import com.android.tv.common.BuildConfig; +// AOSP_Comment_Out import com.android.tv.common.flags.LiveChannels; /** * Set of experiments visible in AOSP. @@ -26,17 +27,15 @@ import com.android.tv.common.BuildConfig; * <p>This file is maintained by hand. */ public final class Experiments { - public static final ExperimentFlag<Boolean> CLOUD_EPG = - ExperimentFlag.createFlag( - true); - public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS = ExperimentFlag.createFlag( +// AOSP_Comment_Out LiveChannels::enableUnratedContentSettings, false); /** Turn analytics on or off based on the System Checkbox for logging. */ public static final ExperimentFlag<Boolean> ENABLE_ANALYTICS_VIA_CHECKBOX = createFlag( +// AOSP_Comment_Out LiveChannels::enableAnalyticsViaCheckbox, false); /** @@ -46,6 +45,7 @@ public final class Experiments { */ public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES = ExperimentFlag.createFlag( +// AOSP_Comment_Out LiveChannels::enableDeveloperFeatures, BuildConfig.ENG); /** @@ -57,6 +57,7 @@ public final class Experiments { */ public static final ExperimentFlag<Boolean> ENABLE_QA_FEATURES = ExperimentFlag.createFlag( +// AOSP_Comment_Out LiveChannels::enableQaFeatures, false); private Experiments() {} diff --git a/common/src/com/android/tv/common/feature/EngOnlyFeature.java b/common/src/com/android/tv/common/feature/BuildTypeFeature.java index 5feb5481..9e1704e8 100644 --- a/common/src/com/android/tv/common/feature/EngOnlyFeature.java +++ b/common/src/com/android/tv/common/feature/BuildTypeFeature.java @@ -20,18 +20,23 @@ import android.content.Context; import com.android.tv.common.BuildConfig; /** A feature that is only available on {@link BuildConfig#ENG} builds. */ -public final class EngOnlyFeature implements Feature { - public static final Feature ENG_ONLY_FEATURE = new EngOnlyFeature(); +public final class BuildTypeFeature implements Feature { + public static final Feature ENG_ONLY_FEATURE = new BuildTypeFeature(BuildConfig.ENG); + public static final Feature ASOP_FEATURE = new BuildTypeFeature(BuildConfig.AOSP); - private EngOnlyFeature() {} + private final boolean mIsBuildType; + + private BuildTypeFeature(boolean isBuildType) { + mIsBuildType = isBuildType; + } @Override public boolean isEnabled(Context context) { - return BuildConfig.ENG; + return mIsBuildType; } @Override public String toString() { - return "EngOnlyFeature(" + BuildConfig.ENG + ")"; + return getClass().getSimpleName() + "(" + mIsBuildType + ")"; } } diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java index 1fceabb3..04052a7c 100644 --- a/common/src/com/android/tv/common/feature/CommonFeatures.java +++ b/common/src/com/android/tv/common/feature/CommonFeatures.java @@ -16,18 +16,16 @@ package com.android.tv.common.feature; -import static com.android.tv.common.feature.FeatureUtils.AND; +import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE; +import static com.android.tv.common.feature.FeatureUtils.and; +import static com.android.tv.common.feature.FeatureUtils.or; import static com.android.tv.common.feature.TestableFeature.createTestableFeature; import android.content.Context; -import android.os.Build; -import android.text.TextUtils; import android.util.Log; -import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig; -import com.android.tv.common.experiments.Experiments; - -import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.flags.has.HasCloudEpgFlags; import com.android.tv.common.util.LocationUtils; +import com.android.tv.common.flags.CloudEpgFlags; /** * List of {@link Feature} that affect more than just the Live TV app. @@ -46,7 +44,7 @@ public class CommonFeatures { * <p>DVR API is introduced in N, it only works when app runs as a system app. */ public static final TestableFeature DVR = - createTestableFeature(AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE)); + createTestableFeature(and(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE)); /** * ENABLE_RECORDING_REGARDLESS_OF_STORAGE_STATUS @@ -56,44 +54,32 @@ public class CommonFeatures { public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE = PropertyFeature.create("force_recording_until_no_space", false); - public static final Feature TUNER = - new Feature() { - @Override - public boolean isEnabled(Context context) { - - if (CommonUtils.isDeveloper()) { - // we enable tuner for developers to test tuner in any platform. - return true; - } - - // This is special handling just for USB Tuner. - // It does not require any N API's but relies on a improvements in N for AC3 - // support - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; - } - }; - /** Show postal code fragment before channel scan. */ public static final Feature ENABLE_CLOUD_EPG_REGION = - new Feature() { - private final String[] supportedRegions = { - }; + or( + FlagFeature.from(HasCloudEpgFlags::fromContext, CloudEpgFlags::supportedRegion), + new Feature() { + private final String[] supportedRegions = { +// AOSP_Comment_Out "US", "GB" + }; - - @Override - public boolean isEnabled(Context context) { - if (!Experiments.CLOUD_EPG.get()) { - if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false"); - return false; - } - String country = LocationUtils.getCurrentCountry(context); - for (int i = 0; i < supportedRegions.length; i++) { - if (supportedRegions[i].equalsIgnoreCase(country)) { - return true; + @Override + public boolean isEnabled(Context context) { + String country = LocationUtils.getCurrentCountry(context); + for (int i = 0; i < supportedRegions.length; i++) { + if (supportedRegions[i].equalsIgnoreCase(country)) { + return true; + } + } + if (DEBUG) Log.d(TAG, "EPG flag false after country check"); + return false; } - } - if (DEBUG) Log.d(TAG, "EPG flag false after country check"); - return false; - } - }; + }); + + // TODO(b/74197177): remove when UI and API finalized. + /** Show channel signal strength. */ + public static final Feature TUNER_SIGNAL_STRENGTH = ENG_ONLY_FEATURE; + + /** Use AudioOnlyTvService for audio-only inputs. */ + public static final Feature ENABLE_TV_SERVICE = ENG_ONLY_FEATURE; } diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java index 8650d151..aaed6c82 100644 --- a/common/src/com/android/tv/common/feature/FeatureUtils.java +++ b/common/src/com/android/tv/common/feature/FeatureUtils.java @@ -17,6 +17,7 @@ package com.android.tv.common.feature; import android.content.Context; +import com.android.tv.common.BuildConfig; import com.android.tv.common.util.CommonUtils; import java.util.Arrays; @@ -28,7 +29,7 @@ public class FeatureUtils { * * @param features the features to or */ - public static Feature OR(final Feature... features) { + public static Feature or(final Feature... features) { return new Feature() { @Override public boolean isEnabled(Context context) { @@ -52,7 +53,7 @@ public class FeatureUtils { * * @param features the features to and */ - public static Feature AND(final Feature... features) { + public static Feature and(final Feature... features) { return new Feature() { @Override public boolean isEnabled(Context context) { @@ -70,6 +71,42 @@ public class FeatureUtils { } }; } + /** + * A feature available in AOSP. + * + * @param googleFeature the feature used in non AOSP builds + * @param aospFeature the feature used in AOSP builds + */ + public static Feature aospFeature( +// AOSP_Comment_Out final Feature googleFeature, + final Feature aospFeature) { + /* Begin_AOSP_Comment_Out + if (!BuildConfig.AOSP) { + return googleFeature; + } else { + End_AOSP_Comment_Out */ + return aospFeature; +// AOSP_Comment_Out } + } + + /** + * Returns a feature that is opposite of the given {@code feature}. + * + * @param feature the feature to invert + */ + public static Feature not(final Feature feature) { + return new Feature() { + @Override + public boolean isEnabled(Context context) { + return !feature.isEnabled(context); + } + + @Override + public String toString() { + return "not(" + feature + ")"; + } + }; + } /** A feature that is always enabled. */ public static final Feature ON = diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/FlagFeature.java index 1d7d1156..8da470ef 100644 --- a/common/src/com/android/tv/common/feature/GServiceFeature.java +++ b/common/src/com/android/tv/common/feature/FlagFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -13,31 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License */ - package com.android.tv.common.feature; import android.content.Context; +import com.google.common.base.Function; + +/** Feature from a Flag */ +public class FlagFeature<T> implements Feature { -/** A feature controlled by a GServices flag. */ -public class GServiceFeature implements Feature { - private static final String LIVECHANNELS_PREFIX = "livechannels:"; - private final String mKey; - private final boolean mDefaultValue; + private final Function<Context, T> mToFlag; + private final Function<T, Boolean> mToBoolean; - public GServiceFeature(String key, boolean defaultValue) { - mKey = LIVECHANNELS_PREFIX + key; - mDefaultValue = defaultValue; + public static <T> FlagFeature<T> from( + Function<Context, T> toFlag, Function<T, Boolean> toBoolean) { + return new FlagFeature<T>(toFlag, toBoolean); + } + + private FlagFeature(Function<Context, T> toFlag, Function<T, Boolean> toBoolean) { + mToFlag = toFlag; + mToBoolean = toBoolean; } @Override public boolean isEnabled(Context context) { - - // GServices is not available outside of Google. - return mDefaultValue; + return mToBoolean.apply(mToFlag.apply(context)); } @Override public String toString() { - return "GService[hash=" + mKey.hashCode() + "]"; + return mToBoolean.toString(); } } diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java index 155b391d..4b0a925f 100644 --- a/common/src/com/android/tv/common/feature/Sdk.java +++ b/common/src/com/android/tv/common/feature/Sdk.java @@ -17,25 +17,33 @@ package com.android.tv.common.feature; import android.content.Context; -import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; /** Holder for SDK version features */ public final class Sdk { - public static final Feature AT_LEAST_N = - new Feature() { - @Override - public boolean isEnabled(Context context) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; - } - }; - - public static final Feature AT_LEAST_O = - new Feature() { - @Override - public boolean isEnabled(Context context) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - } - }; + + public static final Feature AT_LEAST_M = new AtLeast(VERSION_CODES.M); + + public static final Feature AT_LEAST_N = new AtLeast(VERSION_CODES.N); + + public static final Feature AT_LEAST_O = new AtLeast(VERSION_CODES.O); + + public static final Feature AT_LEAST_P = new AtLeast(VERSION_CODES.P); // AOSP_OC:strip_line + + private static final class AtLeast implements Feature { + + private final int versionCode; + + private AtLeast(int versionCode) { + this.versionCode = versionCode; + } + + @Override + public boolean isEnabled(Context unused) { + return VERSION.SDK_INT >= versionCode; + } + } private Sdk() {} } diff --git a/common/src/com/android/tv/common/flags/BackendKnobsFlags.java b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java new file mode 100644 index 00000000..69bac7a0 --- /dev/null +++ b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 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.common.flags; + +/** Flags for tuning non ui behavior */ +public interface BackendKnobsFlags { + + /** + * Whether or not this feature is compiled into this build. + * + * <p>This returns true by default, unless the is_compiled_selector parameter was set during + * code generation. + */ + boolean compiled(); + + /** Enable fetching only part of the program data. */ + boolean enablePartialProgramFetch(); + + /** EPG fetcher interval in hours */ + long epgFetcherIntervalHour(); + + /** Target channel count for EPG. It is used to adjust the EPG length */ + long epgTargetChannelCount(); + + /** Enables fetching a few hours of programs only when the epg is scrolled to that time. */ + boolean fetchProgramsAsNeeded(); + + /** How many hours of programs are loaded in the program guide for during the initial fetch */ + long programGuideInitialFetchHours(); + + /** How many hours of programs are loaded in the program guide */ + long programGuideMaxHours(); +} diff --git a/common/src/com/android/tv/common/flags/CloudEpgFlags.java b/common/src/com/android/tv/common/flags/CloudEpgFlags.java new file mode 100755 index 00000000..ab4c6a17 --- /dev/null +++ b/common/src/com/android/tv/common/flags/CloudEpgFlags.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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.common.flags; + +/** Flags for Cloud EPG */ +public interface CloudEpgFlags { + + /** + * Whether or not this feature is compiled into this build. + * + * <p>This returns true by default, unless the is_compiled_selector parameter was set during + * code generation. + */ + boolean compiled(); + + /** Is the device in a region supported by Cloud Epg */ + boolean supportedRegion(); + + /** List of input ids that Live TV will update their EPG. */ + String thirdPartyEpgInputsCsv(); +} diff --git a/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java new file mode 100755 index 00000000..1afff793 --- /dev/null +++ b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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.common.flags; + +/** Flags allowing concurrent DVR playback */ +public interface ConcurrentDvrPlaybackFlags { + + /** + * Whether or not this feature is compiled into this build. + * + * <p>This returns true by default, unless the is_compiled_selector parameter was set during + * code generation. + */ + boolean compiled(); + + /** Enable playback of DVR playback during recording */ + boolean enabled(); + + /** Enable tuner using recording data for playback in onTune */ + boolean onTuneUsesRecording(); +} diff --git a/common/src/com/android/tv/common/flags/TunerFlags.java b/common/src/com/android/tv/common/flags/TunerFlags.java new file mode 100755 index 00000000..5f899b90 --- /dev/null +++ b/common/src/com/android/tv/common/flags/TunerFlags.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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.common.flags; + +/** Flags for tuner */ +public interface TunerFlags { + + /** + * Whether or not this feature is compiled into this build. + * + * <p>This returns true by default, unless the is_compiled_selector parameter was set during + * code generation. + */ + boolean compiled(); + + /** Tune using current recording if available. */ + boolean tuneUsingRecording(); + + /** Enable using exoplayer V2 */ + boolean useExoplayerV2(); +} diff --git a/common/src/com/android/tv/common/flags/UiFlags.java b/common/src/com/android/tv/common/flags/UiFlags.java new file mode 100755 index 00000000..4c88d08a --- /dev/null +++ b/common/src/com/android/tv/common/flags/UiFlags.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.common.flags; + +/** Flags for Live TV UI */ +public interface UiFlags { + + /** + * Whether or not this feature is compiled into this build. + * + * <p>This returns true by default, unless the is_compiled_selector parameter was set during + * code generation. + */ + boolean compiled(); + + /** + * Number of days to be shown by Recording History. + * + * <p>Set to 0 for all recordings. + */ + long maxHistoryDays(); + + /** Unhide the launcher all the time */ + boolean uhideLauncher(); + + /** Use the Leanback Pin Picker */ + boolean useLeanbackPinPicker(); +} diff --git a/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java new file mode 100644 index 00000000..c33c5528 --- /dev/null +++ b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 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.common.flags.has; + +import android.content.Context; +import com.android.tv.common.flags.CloudEpgFlags; + +/** Has {@link CloudEpgFlags} */ +public interface HasCloudEpgFlags { + + static CloudEpgFlags fromContext(Context context) { + return ((HasCloudEpgFlags) HasUtils.getApplicationContext(context)).getCloudEpgFlags(); + } + + CloudEpgFlags getCloudEpgFlags(); +} diff --git a/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java new file mode 100644 index 00000000..b4710875 --- /dev/null +++ b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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.common.flags.has; + +import android.content.Context; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; + +/** Has {@link ConcurrentDvrPlaybackFlags} */ +public interface HasConcurrentDvrPlaybackFlags { + + static ConcurrentDvrPlaybackFlags fromContext(Context context) { + return ((HasConcurrentDvrPlaybackFlags) HasUtils.getApplicationContext(context)) + .getConcurrentDvrPlaybackFlags(); + } + + ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags(); +} diff --git a/common/src/com/android/tv/common/flags/has/HasUiFlags.java b/common/src/com/android/tv/common/flags/has/HasUiFlags.java new file mode 100644 index 00000000..72cc84f2 --- /dev/null +++ b/common/src/com/android/tv/common/flags/has/HasUiFlags.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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.common.flags.has; + +import com.android.tv.common.flags.UiFlags; + +/** Has {@link UiFlags} */ +public interface HasUiFlags { + + UiFlags getUiFlags(); +} diff --git a/common/src/com/android/tv/common/flags/has/HasUtils.java b/common/src/com/android/tv/common/flags/has/HasUtils.java new file mode 100644 index 00000000..1c6126dc --- /dev/null +++ b/common/src/com/android/tv/common/flags/has/HasUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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.common.flags.has; + +import android.content.Context; + +/** Static utilities for Has interfaces. */ +public final class HasUtils { + + /** Returns the application context. */ + public static Context getApplicationContext(Context context) { + Context appContext = context.getApplicationContext(); + return appContext != null ? appContext : context; + } + + private HasUtils() {} +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java new file mode 100644 index 00000000..a189e473 --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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.common.flags.impl; + +/** Flags for tuning non ui behavior. */ +public final class DefaultBackendKnobsFlags + implements com.android.tv.common.flags.BackendKnobsFlags { + + @Override + public boolean compiled() { + return true; + } + + @Override + public boolean enablePartialProgramFetch() { + return false; + } + + @Override + public long epgFetcherIntervalHour() { + return 25; + } + + @Override + public boolean fetchProgramsAsNeeded() { + return false; + } + + @Override + public long programGuideInitialFetchHours() { + return 8; + } + + @Override + public long programGuideMaxHours() { + return 336; + } + + @Override + public long epgTargetChannelCount() { + return 100; + } +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java new file mode 100644 index 00000000..34c4fc4b --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.common.flags.impl; + +import com.android.tv.common.flags.CloudEpgFlags; + +/** Default flags for Cloud EPG */ +public final class DefaultCloudEpgFlags implements CloudEpgFlags { + + private String mThirdPartyEpgInputCsv = + "com.google.android.tv/.tuner.tvinput.TunerTvInputService," + + "com.technicolor.skipper.tuner/.tvinput.TunerTvInputService," + + "com.silicondust.view/.tif.SDTvInputService"; + + @Override + public boolean compiled() { + return true; + } + + @Override + public boolean supportedRegion() { + return false; + } + + public void setThirdPartyEpgInputCsv(String value) { + mThirdPartyEpgInputCsv = value; + } + + @Override + public String thirdPartyEpgInputsCsv() { + return mThirdPartyEpgInputCsv; + } +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java new file mode 100644 index 00000000..8d8c584a --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.common.flags.impl; + +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; + +/** Default flags for Concurrent DVR Playback */ +public final class DefaultConcurrentDvrPlaybackFlags implements ConcurrentDvrPlaybackFlags { + + @Override + public boolean compiled() { + return true; + } + + @Override + public boolean enabled() { + return false; + } + + @Override + public boolean onTuneUsesRecording() { + return false; + } +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java new file mode 100644 index 00000000..49352364 --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 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.common.flags.impl; + +import dagger.Module; +import dagger.Provides; +import dagger.Reusable; +import com.android.tv.common.flags.BackendKnobsFlags; +import com.android.tv.common.flags.CloudEpgFlags; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; +import com.android.tv.common.flags.TunerFlags; +import com.android.tv.common.flags.UiFlags; + +/** Provides default flags. */ +@Module +public class DefaultFlagsModule { + + @Provides + @Reusable + BackendKnobsFlags provideBackendKnobsFlags() { + return new DefaultBackendKnobsFlags(); + } + + @Provides + @Reusable + CloudEpgFlags provideCloudEpgFlags() { + return new DefaultCloudEpgFlags(); + } + + @Provides + @Reusable + ConcurrentDvrPlaybackFlags provideConcurrentDvrPlaybackFlags() { + return new DefaultConcurrentDvrPlaybackFlags(); + } + + @Provides + @Reusable + TunerFlags provideTunerFlags() { + return new DefaultTunerFlags(); + } + + @Provides + @Reusable + UiFlags provideUiFlags() { + return new DefaultUiFlags(); + } +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java new file mode 100644 index 00000000..195953bc --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.common.flags.impl; + +import com.android.tv.common.flags.TunerFlags; + +/** Default Flags for Tuner */ +public class DefaultTunerFlags implements TunerFlags { + + @Override + public boolean compiled() { + return true; + } + + @Override + public boolean tuneUsingRecording() { + return false; + } + + @Override + public boolean useExoplayerV2() { + return false; + } +} diff --git a/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java new file mode 100644 index 00000000..fce45853 --- /dev/null +++ b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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.common.flags.impl; + +import com.android.tv.common.flags.UiFlags; + +/** Default Flags for Live TV UI */ +public class DefaultUiFlags implements UiFlags { + + @Override + public boolean compiled() { + return true; + } + + @Override + public boolean uhideLauncher() { + return false; + } + + @Override + public boolean useLeanbackPinPicker() { + return false; + } + + @Override + public long maxHistoryDays() { + return 7; + } +} diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java index 8b45a730..0fb864bd 100644 --- a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java +++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java @@ -217,6 +217,7 @@ public class RecordingStorageStatusManager { } } catch (IllegalArgumentException e) { // In rare cases, storage status change was not notified yet. + Log.w(TAG, "Error getting Dvr Storage Status.", e); SoftPreconditions.checkState(false); return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; } @@ -246,7 +247,7 @@ public class RecordingStorageStatusManager { StatFs statFs = new StatFs(storageMountedDir.toString()); storageMountedCapacity = statFs.getTotalBytes(); } catch (IllegalArgumentException e) { - Log.e(TAG, "Storage mount status was changed."); + Log.w(TAG, "Storage mount status was changed.", e); storageMounted = false; storageMountedDir = null; } diff --git a/common/src/com/android/tv/common/singletons/HasSingletons.java b/common/src/com/android/tv/common/singletons/HasSingletons.java new file mode 100644 index 00000000..193aed3a --- /dev/null +++ b/common/src/com/android/tv/common/singletons/HasSingletons.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 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.common.singletons; + +import android.content.Context; + +/** + * A type that can know about and supply a singleton, typically a type t such as an android activity + * or application. + */ +public interface HasSingletons<C> { + + @SuppressWarnings("unchecked") // injection + static <C> C get(Class<C> clazz, Context context) { + return ((HasSingletons<C>) context).singletons(); + } + + /** Returns the strongly typed singleton. */ + C singletons(); +} diff --git a/common/src/com/android/tv/common/singletons/HasTvInputId.java b/common/src/com/android/tv/common/singletons/HasTvInputId.java new file mode 100644 index 00000000..4bc0a21c --- /dev/null +++ b/common/src/com/android/tv/common/singletons/HasTvInputId.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 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.common.singletons; + +/** + * Has TunerInputId. + * + * <p>This is used buy both the tuner to get its input id and by the Live TV to get the + * embedded tuner input id. + */ +public interface HasTvInputId { + + String getEmbeddedTunerInputId(); +} diff --git a/common/src/com/android/tv/common/support/README.md b/common/src/com/android/tv/common/support/README.md new file mode 100644 index 00000000..67993f37 --- /dev/null +++ b/common/src/com/android/tv/common/support/README.md @@ -0,0 +1,8 @@ +# Support Libraries + +Packages here are destined to become support libraries. + +Each package should be self contained and only have dependencies on public libraries. + +It if becomes clear a package should not or will not be part of a support library move it to a +different location.
\ No newline at end of file diff --git a/common/src/com/android/tv/common/support/tis/BaseTvInputService.java b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java new file mode 100644 index 00000000..7791550b --- /dev/null +++ b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 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.common.support.tis; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputService; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import com.android.tv.common.support.tis.TifSession.TifSessionFactory; + +/** Abstract TVInputService. */ +public abstract class BaseTvInputService extends TvInputService { + + private static final IntentFilter INTENT_FILTER = new IntentFilter(); + + static { + INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED); + INTENT_FILTER.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED); + } + + @VisibleForTesting + protected final BroadcastReceiver broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED: + case TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED: + for (Session session : getSessionManager().getSessions()) { + if (session instanceof WrappedSession) { + ((WrappedSession) session).onParentalControlsChanged(); + } + } + break; + default: + // do nothing + } + } + }; + + @Nullable + @Override + public final WrappedSession onCreateSession(String inputId) { + SessionManager sessionManager = getSessionManager(); + if (sessionManager.canCreateNewSession()) { + WrappedSession session = + new WrappedSession( + getApplicationContext(), + sessionManager, + getTifSessionFactory(), + inputId); + sessionManager.addSession(session); + return session; + } + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + registerReceiver(broadcastReceiver, INTENT_FILTER); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterReceiver(broadcastReceiver); + } + + protected abstract TifSessionFactory getTifSessionFactory(); + + protected abstract SessionManager getSessionManager(); +} diff --git a/common/src/com/android/tv/common/support/tis/SessionManager.java b/common/src/com/android/tv/common/support/tis/SessionManager.java new file mode 100644 index 00000000..5eeebc80 --- /dev/null +++ b/common/src/com/android/tv/common/support/tis/SessionManager.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.common.support.tis; + +import android.media.tv.TvInputService.Session; +import com.google.common.collect.ImmutableSet; + +/** Manages the number of concurrent sessions, keeping track of when sessions are released. */ +public interface SessionManager { + + void removeSession(Session session); + + void addSession(Session session); + + boolean canCreateNewSession(); + + ImmutableSet<Session> getSessions(); +} diff --git a/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java new file mode 100644 index 00000000..f0636ccc --- /dev/null +++ b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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.common.support.tis; + +import android.media.tv.TvInputService.Session; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.VisibleForTesting; +import android.util.ArraySet; +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; + +/** A simple session manager that allows a maximum number of concurrent session. */ +public final class SimpleSessionManager implements SessionManager { + + private final Set<Session> sessions; + private final int max; + + public SimpleSessionManager(int max) { + this.max = max; + sessions = VERSION.SDK_INT >= VERSION_CODES.M ? new ArraySet<>() : new HashSet<>(); + } + + @Override + public void removeSession(Session session) { + sessions.remove(session); + } + + @Override + public void addSession(Session session) { + sessions.add(session); + } + + @Override + public boolean canCreateNewSession() { + return sessions.size() < max; + } + + @Override + public ImmutableSet<Session> getSessions() { + return ImmutableSet.copyOf(sessions); + } + + @VisibleForTesting + int getSessionCount() { + return sessions.size(); + } +} diff --git a/common/src/com/android/tv/common/support/tis/TifSession.java b/common/src/com/android/tv/common/support/tis/TifSession.java new file mode 100644 index 00000000..61cfe767 --- /dev/null +++ b/common/src/com/android/tv/common/support/tis/TifSession.java @@ -0,0 +1,203 @@ +/* + * Copyright 2018 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.common.support.tis; + +import android.annotation.TargetApi; +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputService.Session; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.View; +import java.util.List; + +/** + * Custom {@link android.media.tv.TvInputService.Session} class that uses delegation and a callback + * to separate it from the TvInputService for easier testing. + */ +public abstract class TifSession { + + private final TifSessionCallbacks callback; + + /** + * Creates TV Input Framework Session with the given callback. + * + * <p>The callback is used to pass notification to the actual {@link + * android.media.tv.TvInputService.Session}. + * + * <p>Pass a mock callback for tests. + */ + protected TifSession(TifSessionCallbacks callback) { + this.callback = callback; + } + + /** + * Called after this session had been created and the callback is attached. + * + * <p>Do not call notify methods in the constructor, instead call them here if needed at + * creation time. eg @{@link Session#notifyTimeShiftStatusChanged(int)}. + */ + public void onSessionCreated() {} + + /** @see Session#onRelease() */ + public void onRelease() {} + + /** @see Session#onSetSurface(Surface) */ + public abstract boolean onSetSurface(@Nullable Surface surface); + + /** @see Session#onSurfaceChanged(int, int, int) */ + public abstract void onSurfaceChanged(int format, int width, int height); + + /** @see Session#onSetStreamVolume(float) */ + public abstract void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume); + + /** @see Session#onTune(Uri) */ + public abstract boolean onTune(Uri channelUri); + + /** @see Session#onSetCaptionEnabled(boolean) */ + public abstract void onSetCaptionEnabled(boolean enabled); + + /** @see Session#onUnblockContent(TvContentRating) */ + public abstract void onUnblockContent(TvContentRating unblockedRating); + + /** @see Session#onTimeShiftGetCurrentPosition() */ + @TargetApi(Build.VERSION_CODES.M) + public long onTimeShiftGetCurrentPosition() { + return TvInputManager.TIME_SHIFT_INVALID_TIME; + } + + /** @see Session#onTimeShiftGetStartPosition() */ + @TargetApi(Build.VERSION_CODES.M) + public long onTimeShiftGetStartPosition() { + return TvInputManager.TIME_SHIFT_INVALID_TIME; + } + + /** @see Session#onTimeShiftPause() */ + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftPause() {} + + /** @see Session#onTimeShiftResume() */ + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftResume() {} + + /** @see Session#onTimeShiftSeekTo(long) */ + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftSeekTo(long timeMs) {} + + /** @see Session#onTimeShiftSetPlaybackParams(PlaybackParams) */ + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftSetPlaybackParams(PlaybackParams params) {} + + public void onParentalControlsChanged() {} + + /** @see Session#notifyChannelRetuned(Uri) */ + public final void notifyChannelRetuned(final Uri channelUri) { + callback.notifyChannelRetuned(channelUri); + } + + /** @see Session#notifyTracksChanged(List) */ + public final void notifyTracksChanged(final List<TvTrackInfo> tracks) { + callback.notifyTracksChanged(tracks); + } + + /** @see Session#notifyTrackSelected(int, String) */ + public final void notifyTrackSelected(final int type, final String trackId) { + callback.notifyTrackSelected(type, trackId); + } + + /** @see Session#notifyVideoAvailable() */ + public final void notifyVideoAvailable() { + callback.notifyVideoAvailable(); + } + + /** @see Session#notifyVideoUnavailable(int) */ + public final void notifyVideoUnavailable(final int reason) { + callback.notifyVideoUnavailable(reason); + } + + /** @see Session#notifyContentAllowed() */ + public final void notifyContentAllowed() { + callback.notifyContentAllowed(); + } + + /** @see Session#notifyContentBlocked(TvContentRating) */ + public final void notifyContentBlocked(@NonNull final TvContentRating rating) { + callback.notifyContentBlocked(rating); + } + + /** @see Session#notifyTimeShiftStatusChanged(int) */ + @TargetApi(VERSION_CODES.M) + public final void notifyTimeShiftStatusChanged(final int status) { + callback.notifyTimeShiftStatusChanged(status); + } + + /** @see Session#setOverlayViewEnabled(boolean) */ + public void setOverlayViewEnabled(boolean enabled) { + callback.setOverlayViewEnabled(enabled); + } + + /** @see Session#onCreateOverlayView() */ + public View onCreateOverlayView() { + return null; + } + + /** @see Session#onOverlayViewSizeChanged(int, int) */ + public void onOverlayViewSizeChanged(int width, int height) {} + + /** + * Callbacks used to notify the {@link android.media.tv.TvInputService.Session}. + * + * <p>This is implemented internally by {@link WrappedSession}, and can be mocked for tests. + */ + public interface TifSessionCallbacks { + /** @see Session#notifyChannelRetuned(Uri) */ + void notifyChannelRetuned(final Uri channelUri); + /** @see Session#notifyTracksChanged(List) */ + void notifyTracksChanged(final List<TvTrackInfo> tracks); + /** @see Session#notifyTrackSelected(int, String) */ + void notifyTrackSelected(final int type, final String trackId); + /** @see Session#notifyVideoAvailable() */ + void notifyVideoAvailable(); + /** @see Session#notifyVideoUnavailable(int) */ + void notifyVideoUnavailable(final int reason); + /** @see Session#notifyContentAllowed() */ + void notifyContentAllowed(); + /** @see Session#notifyContentBlocked(TvContentRating) */ + void notifyContentBlocked(@NonNull final TvContentRating rating); + /** @see Session#notifyTimeShiftStatusChanged(int) */ + @TargetApi(VERSION_CODES.M) + void notifyTimeShiftStatusChanged(final int status); + /** @see Session#setOverlayViewEnabled(boolean) */ + void setOverlayViewEnabled(boolean enabled); + } + + /** + * Creates a {@link TifSession}. + * + * <p>This is used by {@link WrappedSession} to create the desired {@code TifSession}. Should be + * used with <a href="http://go/autofactory">go/autofactory</a>. + */ + public interface TifSessionFactory { + TifSession create(TifSessionCallbacks callbacks, String inputId); + } +} diff --git a/common/src/com/android/tv/common/support/tis/WrappedSession.java b/common/src/com/android/tv/common/support/tis/WrappedSession.java new file mode 100644 index 00000000..f4a71dda --- /dev/null +++ b/common/src/com/android/tv/common/support/tis/WrappedSession.java @@ -0,0 +1,148 @@ +/* + * Copyright 2018 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.common.support.tis; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputService.Session; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.FloatRange; +import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.View; +import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks; +import com.android.tv.common.support.tis.TifSession.TifSessionFactory; + +/** + * Delegates all call to a {@link TifSession} and removes the session from the {@link + * SessionManager} when {@link Session#onRelease()} is called. + */ +final class WrappedSession extends Session implements TifSessionCallbacks { + + private final SessionManager listener; + private final TifSession delegate; + + WrappedSession( + Context context, + SessionManager sessionManager, + TifSessionFactory sessionFactory, + String inputId) { + super(context); + this.listener = sessionManager; + this.delegate = sessionFactory.create(this, inputId); + } + + @Override + public void onRelease() { + delegate.onRelease(); + listener.removeSession(this); + } + + @Override + public boolean onSetSurface(@Nullable Surface surface) { + return delegate.onSetSurface(surface); + } + + @Override + public void onSurfaceChanged(int format, int width, int height) { + delegate.onSurfaceChanged(format, width, height); + } + + @Override + public void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) { + delegate.onSetStreamVolume(volume); + } + + @Override + public boolean onTune(Uri channelUri) { + return delegate.onTune(channelUri); + } + + @Override + public void onSetCaptionEnabled(boolean enabled) { + delegate.onSetCaptionEnabled(enabled); + } + + @Override + public void onUnblockContent(TvContentRating unblockedRating) { + delegate.onUnblockContent(unblockedRating); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public long onTimeShiftGetCurrentPosition() { + return delegate.onTimeShiftGetCurrentPosition(); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public long onTimeShiftGetStartPosition() { + return delegate.onTimeShiftGetStartPosition(); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftPause() { + delegate.onTimeShiftPause(); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftResume() { + delegate.onTimeShiftResume(); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftSeekTo(long timeMs) { + delegate.onTimeShiftSeekTo(timeMs); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onTimeShiftSetPlaybackParams(PlaybackParams params) { + delegate.onTimeShiftSetPlaybackParams(params); + } + + public void onParentalControlsChanged() { + delegate.onParentalControlsChanged(); + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void notifyTimeShiftStatusChanged(int status) { + // TODO(nchalko): why is the required for call from TisSession.onSessionCreated to work + super.notifyTimeShiftStatusChanged(status); + } + + @Override + public void setOverlayViewEnabled(boolean enabled) { + super.setOverlayViewEnabled(enabled); + } + + @Override + public View onCreateOverlayView() { + return delegate.onCreateOverlayView(); + } + + @Override + public void onOverlayViewSizeChanged(int width, int height) { + delegate.onOverlayViewSizeChanged(width, height); + } +} diff --git a/common/src/com/android/tv/common/ui/setup/SetupActivity.java b/common/src/com/android/tv/common/ui/setup/SetupActivity.java index 67418ce0..1a3ddbda 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupActivity.java +++ b/common/src/com/android/tv/common/ui/setup/SetupActivity.java @@ -16,7 +16,6 @@ package com.android.tv.common.ui.setup; -import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; import android.os.Bundle; @@ -27,10 +26,10 @@ import android.support.annotation.NonNull; import android.transition.Transition; import android.transition.TransitionInflater; import android.view.View; -import android.view.ViewTreeObserver.OnPreDrawListener; import com.android.tv.common.R; import com.android.tv.common.WeakHandler; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; +import dagger.android.DaggerActivity; /** * Setup activity for onboarding screens or TIS. @@ -38,7 +37,7 @@ import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; * <p>The inherited class should add theme {@code Theme.Setup.GuidedStep} to its definition in * AndroidManifest.xml. */ -public abstract class SetupActivity extends Activity implements OnActionClickListener { +public abstract class SetupActivity extends DaggerActivity implements OnActionClickListener { private static final int MSG_EXECUTE_ACTION = 1; private boolean mShowInitialFragment = true; @@ -55,23 +54,7 @@ public abstract class SetupActivity extends Activity implements OnActionClickLis // Show initial fragment only when the saved state is not restored, because the last // fragment is restored if savesInstanceState is not null. if (savedInstanceState == null) { - // This is the workaround to show the first fragment with delay to show the fragment - // enter transition. See http://b/26255145 - getWindow() - .getDecorView() - .getViewTreeObserver() - .addOnPreDrawListener( - new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - getWindow() - .getDecorView() - .getViewTreeObserver() - .removeOnPreDrawListener(this); - showInitialFragment(); - return true; - } - }); + showInitialFragment(); } else { mShowInitialFragment = false; } diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java index 305431d3..4513a879 100644 --- a/common/src/com/android/tv/common/util/CommonUtils.java +++ b/common/src/com/android/tv/common/util/CommonUtils.java @@ -138,14 +138,23 @@ public final class CommonUtils { return ISO_8601.get().format(new Date(timeMillis)); } - /** Deletes a file or a directory. */ - public static void deleteDirOrFile(File fileOrDirectory) { + /** + * Deletes a file or a directory. + * + * @return <code>true</code> if and only if the file or directory is successfully deleted; + * <code>false</code> otherwise + */ + public static boolean deleteDirOrFile(File fileOrDirectory) { if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { - deleteDirOrFile(child); + File[] files = fileOrDirectory.listFiles(); + if (files != null) { + for (File child : files) { + deleteDirOrFile(child); + } } } - fileOrDirectory.delete(); + // If earlier deletes failed this will also + return fileOrDirectory.delete(); } public static boolean isRoboTest() { diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java index 53155298..ee5119eb 100644 --- a/common/src/com/android/tv/common/util/LocationUtils.java +++ b/common/src/com/android/tv/common/util/LocationUtils.java @@ -34,14 +34,20 @@ import com.android.tv.common.BuildConfig; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; /** A utility class to get the current location. */ public class LocationUtils { private static final String TAG = "LocationUtils"; private static final boolean DEBUG = false; + private static final Set<OnUpdateAddressListener> sOnUpdateAddressListeners = + Collections.synchronizedSet(new HashSet<>()); + private static Context sApplicationContext; private static Address sAddress; private static String sCountry; @@ -63,6 +69,39 @@ public class LocationUtils { return null; } + /** The listener used when address is updated. */ + public interface OnUpdateAddressListener { + /** + * Called when address is updated. + * + * This listener is removed when this method returns true. + * + * @return {@code true} if the job has been finished and the listener needs to be removed; + * {@code false} otherwise. + */ + boolean onUpdateAddress(Address address); + } + + /** + * Add an {@link OnUpdateAddressListener} instance. + * + * Note that the listener is removed automatically when + * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}. + */ + public static void addOnUpdateAddressListener(OnUpdateAddressListener listener) { + sOnUpdateAddressListeners.add(listener); + } + + /** + * Remove an {@link OnUpdateAddressListener} instance if it exists. + * + * Note that the listener will be removed automatically when + * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}. + */ + public static void removeOnUpdateAddressListener(OnUpdateAddressListener listener) { + sOnUpdateAddressListeners.remove(listener); + } + /** Returns the current country. */ @NonNull public static synchronized String getCurrentCountry(Context context) { @@ -92,6 +131,17 @@ public class LocationUtils { } catch (Exception e) { // Do nothing } + Set<OnUpdateAddressListener> listenersToRemove = new HashSet<>(); + synchronized (sOnUpdateAddressListeners) { + for (OnUpdateAddressListener listener : sOnUpdateAddressListeners) { + if (listener.onUpdateAddress(sAddress)) { + listenersToRemove.add(listener); + } + } + for (OnUpdateAddressListener listener : listenersToRemove) { + removeOnUpdateAddressListener(listener); + } + } } else { if (DEBUG) Log.d(TAG, "No address returned"); } diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java index 91f2bcd1..3c94aed6 100644 --- a/common/src/com/android/tv/common/util/NetworkTrafficTags.java +++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java @@ -43,19 +43,16 @@ public final class NetworkTrafficTags { @Override public void execute(final @NonNull Runnable command) { - // TODO(b/62038127): robolectric does not support lamdas in unbundled apps - delegateExecutor.execute( - new Runnable() { - @Override - public void run() { - TrafficStats.setThreadStatsTag(tag); - try { - command.run(); - } finally { - TrafficStats.clearThreadStatsTag(); - } - } - }); + // TODO(b/62038127): robolectric does not support lamdas in unbundled apps + delegateExecutor.execute( + () -> { + TrafficStats.setThreadStatsTag(tag); + try { + command.run(); + } finally { + TrafficStats.clearThreadStatsTag(); + } + }); } } diff --git a/common/src/com/android/tv/common/util/PermissionUtils.java b/common/src/com/android/tv/common/util/PermissionUtils.java index 8d409e50..ca1abdc4 100644 --- a/common/src/com/android/tv/common/util/PermissionUtils.java +++ b/common/src/com/android/tv/common/util/PermissionUtils.java @@ -65,4 +65,9 @@ public class PermissionUtils { return context.checkSelfPermission("android.permission.INTERNET") == PackageManager.PERMISSION_GRANTED; } + + public static boolean hasWriteExternalStorage(Context context) { + return context.checkSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE") + == PackageManager.PERMISSION_GRANTED; + } } diff --git a/common/src/com/android/tv/common/util/StringUtils.java b/common/src/com/android/tv/common/util/StringUtils.java index b9461426..bc826208 100644 --- a/common/src/com/android/tv/common/util/StringUtils.java +++ b/common/src/com/android/tv/common/util/StringUtils.java @@ -31,4 +31,9 @@ public final class StringUtils { } return a.compareTo(b); } + + /** Returns {@code s} or {@code ""} if {@code s} is {@code null} */ + public static final String nullToEmpty(String s) { + return s == null ? "" : s; + } } diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java index a9f18d4b..6ac2907b 100644 --- a/common/src/com/android/tv/common/util/SystemProperties.java +++ b/common/src/com/android/tv/common/util/SystemProperties.java @@ -40,6 +40,10 @@ public final class SystemProperties { public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty("tv_use_tracker", true); + /** Allow third party inputs. */ + public static final BooleanSystemProperty ALLOW_THIRD_PARTY_INPUTS = + new BooleanSystemProperty("ro.tv_allow_third_party_inputs", true); + static { updateSystemProperties(); } diff --git a/jni/gen_jni.sh b/gradle.properties index c06b7b91..62082346 100755..100644 --- a/jni/gen_jni.sh +++ b/gradle.properties @@ -1,6 +1,4 @@ -#!/bin/bash -# -# Copyright (C) 2017 The Android Open Source Project +# Copyright (C) 2018 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. @@ -14,5 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +# +# Experimental gradle configuration. This file may not be up to date. +# + + +org.gradle.jvmargs=-Xmx6144m -XX:MaxPermSize=6144m -XX:+HeapDumpOnOutOfMemoryError -javah -jni -classpath ../../bin/classes:../../../../../../prebuilts/sdk/current/public/android.jar -o tunertvinput_jni.h com.android.tv.tuner.TunerHal +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true
\ No newline at end of file diff --git a/jni/Android.bp b/jni/Android.bp new file mode 100644 index 00000000..bbf27787 --- /dev/null +++ b/jni/Android.bp @@ -0,0 +1,30 @@ +// +// Copyright (C) 2019 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. +// + +cc_library_shared { + name: "libtunertvinput_jni", + srcs: [ + "tunertvinput_jni.cpp", + "DvbManager.cpp", + ], + cflags: [ + "-Wall", + "-Werror", + ], + sdk_version: "23", + stl: "c++_static", + shared_libs: ["liblog"], +} diff --git a/jni/Android.mk b/jni/Android.mk deleted file mode 100644 index cfc8623e..00000000 --- a/jni/Android.mk +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (C) 2017 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. -# - -LOCAL_PATH := $(call my-dir) - -# -------------------------------------------------------------- -include $(CLEAR_VARS) - -LOCAL_MODULE := libtunertvinput_jni -LOCAL_SRC_FILES += tunertvinput_jni.cpp DvbManager.cpp -LOCAL_CFLAGS := -Wall -Werror -LOCAL_SDK_VERSION := 23 -LOCAL_NDK_STL_VARIANT := c++_static -LOCAL_LDLIBS := -llog - -include $(BUILD_SHARED_LIBRARY) -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/jni/DvbManager.cpp b/jni/DvbManager.cpp index 8e51999c..f9dff59b 100644 --- a/jni/DvbManager.cpp +++ b/jni/DvbManager.cpp @@ -82,6 +82,37 @@ bool DvbManager::isFeLocked() { return false; } +// This function gets the signal strength from tuner. +// Output can be: +// -3 means File Descriptor invalid, +// or DVB version is not supported, +// or ERROR while communicate with hardware via ioctl. +// int signal returns the raw signal strength value. +int DvbManager::getSignalStrength() { + // TODO(b/74197177): add support for DVB V5. + if (mFeFd == -1 || mDvbApiVersion != DVB_API_VERSION3) { + return -3; + } + uint16_t strength = 0; + // ERROR code from ioctl can be: + // EBADF means fd is not a valid open file descriptor + // EFAULT means status points to invalid address + // ENOSIGNAL means there is no signal, thus no meaningful signal strength + // ENOSYS means function not available for this device + // + // The function used to communicate with tuner in DVB v3 is + // ioctl(fd, request, &strength) + // int fd is the File Descriptor, can't be -1 + // int request is the request type, + // FE_READ_SIGNAL_STRENGTH for getting signal strength + // uint16_t *strength stores the strength value returned from tuner + if (ioctl(mFeFd, FE_READ_SIGNAL_STRENGTH, &strength) == -1) { + ALOGD("FE_READ_SIGNAL_STRENGTH failed, %s", strerror(errno)); + return -3; + } + return strength; +} + int DvbManager::tune(JNIEnv *env, jobject thiz, const int frequency, const char *modulationStr, int timeout_ms) { resetExceptFe(); diff --git a/jni/DvbManager.h b/jni/DvbManager.h index 124fa943..b01113e1 100644 --- a/jni/DvbManager.h +++ b/jni/DvbManager.h @@ -49,7 +49,7 @@ class DvbManager { static const int DELIVERY_SYSTEM_ATSC = com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC; static const int DELIVERY_SYSTEM_DVBC = - com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC; + com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC; static const int DELIVERY_SYSTEM_DVBS = com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS; static const int DELIVERY_SYSTEM_DVBS2 = @@ -85,6 +85,7 @@ public: void closeAllDvbPidFilter(); void setHasPendingTune(bool hasPendingTune); int getDeliverySystemType(JNIEnv *env, jobject thiz); + int getSignalStrength(); private: int openDvbFe(JNIEnv *env, jobject thiz); diff --git a/jni/tunertvinput_jni.cpp b/jni/tunertvinput_jni.cpp index 368e2d5b..030f9617 100644 --- a/jni/tunertvinput_jni.cpp +++ b/jni/tunertvinput_jni.cpp @@ -96,6 +96,23 @@ JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeStopTune( /* * Class: com_android_tv_tuner_TunerHal + * Method: nativeGetSignalStrength + * Signature: (J)V + */ +JNIEXPORT int JNICALL +Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength( + JNIEnv *, jobject, jlong deviceId) { + std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId); + if (it != sDvbManagers.end()) { + return it->second->getSignalStrength(); + } + // If DvbManager can't be found, + // return -3 as signal strength not supported. + return -3; +} + +/* + * Class: com_android_tv_tuner_TunerHal * Method: nativeAddPidFilter * Signature: (JII)V */ diff --git a/jni/tunertvinput_jni.h b/jni/tunertvinput_jni.h index d299c304..36e631f5 100644..100755 --- a/jni/tunertvinput_jni.h +++ b/jni/tunertvinput_jni.h @@ -17,20 +17,12 @@ extern "C" { #define com_android_tv_tuner_TunerHal_FILTER_TYPE_VIDEO 2L #undef com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR #define com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR 3L -#undef com_android_tv_tuner_TunerHal_PID_PAT -#define com_android_tv_tuner_TunerHal_PID_PAT 0L -#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE -#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L -#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS -#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L -#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS -#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED 0L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC 1L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC -#define com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC 2L +#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC 2L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS 3L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS2 @@ -39,29 +31,51 @@ extern "C" { #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT 5L #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2 6L +#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN +#define com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN 1L +#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_USB +#define com_android_tv_tuner_TunerHal_TUNER_TYPE_USB 2L +#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK +#define com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK 3L +#undef com_android_tv_tuner_TunerHal_PID_PAT +#define com_android_tv_tuner_TunerHal_PID_PAT 0L +#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE +#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L +#undef com_android_tv_tuner_TunerHal_PID_DVB_SDT +#define com_android_tv_tuner_TunerHal_PID_DVB_SDT 17L +#undef com_android_tv_tuner_TunerHal_PID_DVB_EIT +#define com_android_tv_tuner_TunerHal_PID_DVB_EIT 18L +#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS +#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L +#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS +#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L +#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB +#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB 1L +#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER +#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER 100L /* * Class: com_android_tv_tuner_TunerHal * Method: nativeFinalize * Signature: (J)V */ -JNIEXPORT void JNICALL -Java_com_android_tv_tuner_TunerHal_nativeFinalize(JNIEnv *, jobject, jlong); +JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeFinalize + (JNIEnv *, jobject, jlong); /* * Class: com_android_tv_tuner_TunerHal * Method: nativeTune * Signature: (JILjava/lang/String;I)Z */ -JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune( - JNIEnv *, jobject, jlong, jint, jstring, jint); +JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune + (JNIEnv *, jobject, jlong, jint, jstring, jint); /* * Class: com_android_tv_tuner_TunerHal * Method: nativeAddPidFilter * Signature: (JII)V */ -JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter( - JNIEnv *, jobject, jlong, jint, jint); +JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter + (JNIEnv *, jobject, jlong, jint, jint); /* * Class: com_android_tv_tuner_TunerHal @@ -69,54 +83,52 @@ JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter( * Signature: (J)V */ JNIEXPORT void JNICALL -Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters(JNIEnv *, jobject, - jlong); +Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters + (JNIEnv *, jobject, jlong); /* * Class: com_android_tv_tuner_TunerHal - * Method: nativeStopTune - * Signature: (J)V + * Method: nativeSetHasPendingTune + * Signature: (JZ)V */ JNIEXPORT void JNICALL -Java_com_android_tv_tuner_TunerHal_nativeStopTune(JNIEnv *, jobject, jlong); +Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune + (JNIEnv *, jobject, jlong, jboolean); /* * Class: com_android_tv_tuner_TunerHal - * Method: nativeWriteInBuffer - * Signature: (J[BI)I + * Method: nativeGetDeliverySystemType + * Signature: (J)I */ -JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer( - JNIEnv *, jobject, jlong, jbyteArray, jint); +JNIEXPORT jint JNICALL +Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType + (JNIEnv *, jobject, jlong); /* * Class: com_android_tv_tuner_TunerHal - * Method: nativeSetHasPendingTune - * Signature: (JZ)V + * Method: nativeGetSignalStrength + * Signature: (J)I */ -JNIEXPORT void JNICALL -Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune(JNIEnv *, jobject, - jlong, jboolean); +JNIEXPORT jint JNICALL +Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength + (JNIEnv *, jobject, jlong); /* * Class: com_android_tv_tuner_TunerHal - * Method: nativeGetDeliverySystemType - * Signature: (J)I + * Method: nativeStopTune + * Signature: (J)V */ -JNIEXPORT int JNICALL -Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType(JNIEnv *, - jobject, jlong); +JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeStopTune + (JNIEnv *, jobject, jlong); -#ifdef __cplusplus -} -#endif -#endif -/* Header for class com_android_tv_tuner_TunerHal_FilterType */ +/* + * Class: com_android_tv_tuner_TunerHal + * Method: nativeWriteInBuffer + * Signature: (J[BI)I + */ +JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer + (JNIEnv *, jobject, jlong, jbyteArray, jint); -#ifndef _Included_com_android_tv_tuner_TunerHal_FilterType -#define _Included_com_android_tv_tuner_TunerHal_FilterType -#ifdef __cplusplus -extern "C" { -#endif #ifdef __cplusplus } #endif diff --git a/libs/Android.bp b/libs/Android.bp new file mode 100644 index 00000000..fea94875 --- /dev/null +++ b/libs/Android.bp @@ -0,0 +1,165 @@ +// Copyright (C) 2019 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. + +java_import { + name: "tv-auto-factory-jar", + jars: ["auto-factory-1.0-beta2.jar"], + host_supported: true, + sdk_version: "current", +} + +java_plugin { + name: "tv-auto-factory", + static_libs: [ + "jsr330", + "tv-auto-factory-jar", + "tv-guava-jre-jar", + "tv-javawriter-jar", + "tv-javax-annotations-jar", + ], + processor_class: "com.google.auto.factory.processor.AutoFactoryProcessor", + generates_api: true, +} + + +java_import { + name: "tv-auto-value-jar", + jars: ["auto-value-1.5.2.jar"], + host_supported: true, + sdk_version: "current", +} + +java_plugin { + name: "tv-auto-value", + static_libs: [ + "tv-auto-value-jar", + "tv-guava-jre-jar", + ], + processor_class: "com.google.auto.value.processor.AutoValueProcessor", +} + +java_import { + name: "tv-error-prone-annotations-jar", + jars: ["error_prone_annotations-2.3.1.jar"], + sdk_version: "current", +} + +java_import { + name: "tv-guava-jre-jar", + jars: ["guava-23.3-jre.jar"], + host_supported: true, + sdk_version: "current", +} + +java_import { + name: "tv-guava-android-jar", + jars: ["guava-23.6-android.jar"], + sdk_version: "current", +} + +java_import_host{ + name: "tv-javawriter-jar", + jars: ["javawriter-2.5.1.jar"], +} + +java_import { + name: "tv-javax-annotations-jar", + jars: ["javax.annotation-api-1.2.jar"], + host_supported: true, + sdk_version: "current", +} + + +android_library_import { + name: "tv-lib-exoplayer", + aars: ["exoplayer-r1.5.16.aar"], + sdk_version: "current", +} + +android_library_import { + name: "tv-lib-exoplayer-v2-core", + aars: ["exoplayer-core-2.9.0.aar"], + sdk_version: "current", +} + +java_import_host { + name: "tv-lib-dagger-compiler-deps", + jars: [ + "google-java-format-1.4-all-deps.jar", + "guava-23.3-jre.jar", + "javapoet-1.8.0.jar", + ], +} + +java_import_host { + name: "tv-lib-dagger-compiler-import", + jars: [ + "dagger-compiler-2.15.jar", + "dagger-producers-2.15.jar", + "dagger-spi-2.15.jar", + ], +} + +java_import { + name: "tv-lib-dagger", + jars: ["dagger-2.15.jar"], + host_supported: true, + sdk_version: "current", +} + +java_plugin { + name: "tv-lib-dagger-compiler", + static_libs: [ + "tv-lib-dagger-compiler-import", + "tv-lib-dagger-compiler-deps", + "jsr330", + "tv-lib-dagger", + ], + processor_class: "dagger.internal.codegen.ComponentProcessor", + generates_api: true, +} + +android_library_import { + name: "tv-lib-dagger-android", + aars: ["dagger-android-2.15.aar"], + sdk_version: "current", +} + +java_import_host { + name: "tv-lib-dagger-android-processor-import", + jars: [ + "dagger-android-jarimpl-2.15.jar", + "dagger-android-processor-2.15.jar", + "dagger-android-support-jarimpl-2.15.jar", + ], +} + +java_plugin { + name: "tv-lib-dagger-android-processor", + static_libs: [ + "tv-lib-dagger-android-processor-import", + "tv-lib-dagger-compiler-deps", + "jsr330", + "tv-lib-dagger", + ], + processor_class: "dagger.android.processor.AndroidProcessor", + generates_api: true, +} + +java_import { + name: "tv-lib-truth", + jars: ["truth-0.36.jar"], + host_supported: true, + sdk_version: "current", +} diff --git a/libs/auto-factory-1.0-beta2.jar b/libs/auto-factory-1.0-beta2.jar Binary files differnew file mode 100644 index 00000000..ceaddac7 --- /dev/null +++ b/libs/auto-factory-1.0-beta2.jar diff --git a/libs/auto-value-1.5.2.jar b/libs/auto-value-1.5.2.jar Binary files differnew file mode 100644 index 00000000..8ac05679 --- /dev/null +++ b/libs/auto-value-1.5.2.jar diff --git a/libs/dagger-2.15.jar b/libs/dagger-2.15.jar Binary files differnew file mode 100644 index 00000000..6d766885 --- /dev/null +++ b/libs/dagger-2.15.jar diff --git a/libs/dagger-android-2.15.aar b/libs/dagger-android-2.15.aar Binary files differnew file mode 100644 index 00000000..430294a2 --- /dev/null +++ b/libs/dagger-android-2.15.aar diff --git a/libs/dagger-android-jarimpl-2.15.jar b/libs/dagger-android-jarimpl-2.15.jar Binary files differnew file mode 100644 index 00000000..7f7cd459 --- /dev/null +++ b/libs/dagger-android-jarimpl-2.15.jar diff --git a/libs/dagger-android-processor-2.15.jar b/libs/dagger-android-processor-2.15.jar Binary files differnew file mode 100644 index 00000000..3c7ac054 --- /dev/null +++ b/libs/dagger-android-processor-2.15.jar diff --git a/libs/dagger-android-support-2.15.aar b/libs/dagger-android-support-2.15.aar Binary files differnew file mode 100644 index 00000000..89a71a91 --- /dev/null +++ b/libs/dagger-android-support-2.15.aar diff --git a/libs/dagger-android-support-jarimpl-2.15.jar b/libs/dagger-android-support-jarimpl-2.15.jar Binary files differnew file mode 100644 index 00000000..d0ea01a7 --- /dev/null +++ b/libs/dagger-android-support-jarimpl-2.15.jar diff --git a/libs/dagger-compiler-2.15.jar b/libs/dagger-compiler-2.15.jar Binary files differnew file mode 100644 index 00000000..e73110f6 --- /dev/null +++ b/libs/dagger-compiler-2.15.jar diff --git a/libs/dagger-producers-2.15.jar b/libs/dagger-producers-2.15.jar Binary files differnew file mode 100644 index 00000000..f1dbb07a --- /dev/null +++ b/libs/dagger-producers-2.15.jar diff --git a/libs/dagger-spi-2.15.jar b/libs/dagger-spi-2.15.jar Binary files differnew file mode 100644 index 00000000..6e3156a9 --- /dev/null +++ b/libs/dagger-spi-2.15.jar diff --git a/libs/error_prone_annotations-2.3.1.jar b/libs/error_prone_annotations-2.3.1.jar Binary files differnew file mode 100644 index 00000000..8a0efa37 --- /dev/null +++ b/libs/error_prone_annotations-2.3.1.jar diff --git a/libs/exoplayer-core-2-SNAPHOT-20180114.aar b/libs/exoplayer-core-2-SNAPHOT-20180114.aar Binary files differdeleted file mode 100644 index 90af2e63..00000000 --- a/libs/exoplayer-core-2-SNAPHOT-20180114.aar +++ /dev/null diff --git a/libs/exoplayer-core-2.9.0.aar b/libs/exoplayer-core-2.9.0.aar Binary files differnew file mode 100644 index 00000000..64c4f37f --- /dev/null +++ b/libs/exoplayer-core-2.9.0.aar diff --git a/libs/google-java-format-1.4-all-deps.jar b/libs/google-java-format-1.4-all-deps.jar Binary files differnew file mode 100644 index 00000000..b10bfbd6 --- /dev/null +++ b/libs/google-java-format-1.4-all-deps.jar diff --git a/libs/guava-23.3-jre.jar b/libs/guava-23.3-jre.jar Binary files differnew file mode 100644 index 00000000..b13e275f --- /dev/null +++ b/libs/guava-23.3-jre.jar diff --git a/libs/guava-23.5-jre.jar b/libs/guava-23.5-jre.jar Binary files differnew file mode 100644 index 00000000..7e5f13a8 --- /dev/null +++ b/libs/guava-23.5-jre.jar diff --git a/libs/guava-23.6-android.jar b/libs/guava-23.6-android.jar Binary files differnew file mode 100644 index 00000000..01180d23 --- /dev/null +++ b/libs/guava-23.6-android.jar diff --git a/libs/javapoet-1.8.0.jar b/libs/javapoet-1.8.0.jar Binary files differnew file mode 100644 index 00000000..6758b6d7 --- /dev/null +++ b/libs/javapoet-1.8.0.jar diff --git a/libs/javawriter-2.5.1.jar b/libs/javawriter-2.5.1.jar Binary files differnew file mode 100644 index 00000000..4ec579e7 --- /dev/null +++ b/libs/javawriter-2.5.1.jar diff --git a/libs/javax.annotation-api-1.2.jar b/libs/javax.annotation-api-1.2.jar Binary files differnew file mode 100644 index 00000000..9ab39ffa --- /dev/null +++ b/libs/javax.annotation-api-1.2.jar diff --git a/libs/truth-0.36.jar b/libs/truth-0.36.jar Binary files differnew file mode 100644 index 00000000..8174e4a9 --- /dev/null +++ b/libs/truth-0.36.jar diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png Binary files differnew file mode 100644 index 00000000..76a57d15 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png Binary files differnew file mode 100644 index 00000000..ce42f364 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png diff --git a/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png Binary files differnew file mode 100644 index 00000000..4f967453 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png diff --git a/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png Binary files differnew file mode 100644 index 00000000..0d9bf8b4 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png diff --git a/material_res/drawable-hdpi/quantum_ic_error_white_48.png b/material_res/drawable-hdpi/quantum_ic_error_white_48.png Binary files differnew file mode 100644 index 00000000..abe2573b --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_error_white_48.png diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png Binary files differnew file mode 100644 index 00000000..c7e88489 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png Binary files differnew file mode 100644 index 00000000..3b79370a --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png Binary files differnew file mode 100644 index 00000000..bf736dc8 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png Binary files differnew file mode 100644 index 00000000..8bee424e --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png Binary files differnew file mode 100644 index 00000000..1765e947 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_18.png b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png Binary files differnew file mode 100644 index 00000000..7520b79f --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_96.png b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png Binary files differnew file mode 100644 index 00000000..88c22324 --- /dev/null +++ b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png Binary files differnew file mode 100644 index 00000000..dbfb7ab4 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png Binary files differnew file mode 100644 index 00000000..c39725cb --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png diff --git a/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png Binary files differnew file mode 100644 index 00000000..d36d696e --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png diff --git a/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png Binary files differnew file mode 100644 index 00000000..976a44bc --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png diff --git a/material_res/drawable-mdpi/quantum_ic_error_white_48.png b/material_res/drawable-mdpi/quantum_ic_error_white_48.png Binary files differnew file mode 100644 index 00000000..9829698d --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_error_white_48.png diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png Binary files differnew file mode 100644 index 00000000..a0c5d415 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png Binary files differnew file mode 100644 index 00000000..0e70e98d --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png Binary files differnew file mode 100644 index 00000000..eb2124d0 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png Binary files differnew file mode 100644 index 00000000..a2045c33 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png Binary files differnew file mode 100644 index 00000000..8d76fd42 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_18.png b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png Binary files differnew file mode 100644 index 00000000..1f055176 --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_96.png b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png Binary files differnew file mode 100644 index 00000000..8683a2ea --- /dev/null +++ b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png Binary files differnew file mode 100644 index 00000000..225e4e54 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png Binary files differnew file mode 100644 index 00000000..d7b27da8 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png diff --git a/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png Binary files differnew file mode 100644 index 00000000..2c6e4741 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png diff --git a/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png Binary files differnew file mode 100644 index 00000000..1336147d --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png diff --git a/material_res/drawable-xhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png Binary files differnew file mode 100644 index 00000000..830fb7e1 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png Binary files differnew file mode 100644 index 00000000..26d6bff8 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png Binary files differnew file mode 100644 index 00000000..3b7f7ade --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png Binary files differnew file mode 100644 index 00000000..b80307c3 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png Binary files differnew file mode 100644 index 00000000..1ccc9770 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png Binary files differnew file mode 100644 index 00000000..62501b0b --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png Binary files differnew file mode 100644 index 00000000..55c68431 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png Binary files differnew file mode 100644 index 00000000..23e6d932 --- /dev/null +++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png Binary files differnew file mode 100644 index 00000000..0502223d --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png Binary files differnew file mode 100644 index 00000000..eceb34c7 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png Binary files differnew file mode 100644 index 00000000..980d10b4 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png Binary files differnew file mode 100644 index 00000000..defa1bfe --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png Binary files differnew file mode 100644 index 00000000..c8349523 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png Binary files differnew file mode 100644 index 00000000..2e200fbb --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png Binary files differnew file mode 100644 index 00000000..215f9b73 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png Binary files differnew file mode 100644 index 00000000..c8c0ebfc --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png Binary files differnew file mode 100644 index 00000000..8377811b --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png Binary files differnew file mode 100644 index 00000000..b191b9d8 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png Binary files differnew file mode 100644 index 00000000..fb079e95 --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png Binary files differnew file mode 100644 index 00000000..064cd51a --- /dev/null +++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png Binary files differnew file mode 100644 index 00000000..fe0ecc64 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png Binary files differnew file mode 100644 index 00000000..5e61c3d1 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png Binary files differnew file mode 100644 index 00000000..60463c56 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png Binary files differnew file mode 100644 index 00000000..b316f1d5 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png Binary files differnew file mode 100644 index 00000000..ad4a474a --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png Binary files differnew file mode 100644 index 00000000..f8051327 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png Binary files differnew file mode 100644 index 00000000..42debccf --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png Binary files differnew file mode 100644 index 00000000..6a7f3209 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png Binary files differnew file mode 100644 index 00000000..706da320 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png Binary files differnew file mode 100644 index 00000000..4a3ac28d --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png Binary files differnew file mode 100644 index 00000000..807b9fa1 --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png Binary files differnew file mode 100644 index 00000000..2439be1d --- /dev/null +++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png diff --git a/open_source_project.README b/open_source_project.README index 31532f03..897b8eeb 100644 --- a/open_source_project.README +++ b/open_source_project.README @@ -7,7 +7,7 @@ How to build: https://source.android.com/source/building.html (Developers using PDK can skip the step 1.) 2. Enable the feature PackageManager.FEATURE_LIVE_TV. -3. Put this project under Android platform repository. +3. Put this project under Android platform repository if required. 4. Include this package inside platform build. 5. Build the platform. https://source.android.com/source/building.html diff --git a/partner_support/Android.bp b/partner_support/Android.bp new file mode 100644 index 00000000..4775fc11 --- /dev/null +++ b/partner_support/Android.bp @@ -0,0 +1,32 @@ +// +// Copyright (C) 2019 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. +// + +android_library { + name: "live-channels-partner-support", + srcs: ["src/**/*.java"], + + sdk_version: "system_current", + min_sdk_version: "23", + + resource_dirs: ["res"], + + static_libs: ["android-support-annotations"], + + libs: ["tv-auto-value-jar"], + + plugins: ["tv-auto-value"], + +} diff --git a/partner_support/Android.mk b/partner_support/Android.mk deleted file mode 100644 index 8306921b..00000000 --- a/partner_support/Android.mk +++ /dev/null @@ -1,23 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# Include all java files. -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_MODULE := live-channels-partner-support -LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES -LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := system_current -LOCAL_MIN_SDK_VERSION := 23 - -LOCAL_USE_AAPT2 := true - -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res - -LOCAL_STATIC_JAVA_LIBRARIES := android-support-annotations - -include $(LOCAL_PATH)/buildconfig.mk - -include $(BUILD_STATIC_JAVA_LIBRARY) - -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/partner_support/AndroidManifest.xml b/partner_support/AndroidManifest.xml index 5a45f0db..45a693fc 100644 --- a/partner_support/AndroidManifest.xml +++ b/partner_support/AndroidManifest.xml @@ -17,6 +17,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.tv.partner.support" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application /> </manifest> diff --git a/partner_support/g3doc/SeriesIdColumnForPartners.md b/partner_support/g3doc/SeriesIdColumnForPartners.md new file mode 100644 index 00000000..cd44db03 --- /dev/null +++ b/partner_support/g3doc/SeriesIdColumnForPartners.md @@ -0,0 +1,30 @@ +# 3rd party instructions for using series recording feature of Live Channels + +## Prerequisites + +* Updated agreement with Google +* Oreo or patched Nougat + +## Nougat + +To enable series recording with Nougat you will need the following changes. + +### Patch TVProvider + +To run in Nougat you must backport the following changes + +* [Filter out non-existing customized columns in + DB](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/142162af889b2c124bb012eea608c6a65eed54bb) +* [Add TvProvider methods to get and add + columns](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/cda6788ae903513a555fd3e07a5a1c14218c40a2) + +### Customisation + +Indicate TvProvider is patched by including the following in their TV +customization resource + +``` +<bool name="tvprovider_allows_column_creation">true</bool> +``` + +See https://source.android.com/devices/tv/customize-tv-app diff --git a/partner_support/g3doc/TurnOffEmbeddedTuner.md b/partner_support/g3doc/TurnOffEmbeddedTuner.md new file mode 100644 index 00000000..0ba7cff2 --- /dev/null +++ b/partner_support/g3doc/TurnOffEmbeddedTuner.md @@ -0,0 +1,15 @@ +# 3rd party instructions turning off the embedded tuner in Live Channels + +Partners that have a built in tuner should provide a TV Input like +SampleDvbTuner. When partners provide their own tuner they MUST turn of the +embedded tuner in Live Channels. + +### Customisation + +Indicate Live Channels should not use it's embedded tuner implementation. + +``` +<bool name="turn_off_embedded_tuner">true</bool> +``` + +See https://source.android.com/devices/tv/customize-tv-app diff --git a/partner_support/sample_customization/AndroidManifest.xml b/partner_support/sample_customization/AndroidManifest.xml index f1edad3f..804691a6 100644 --- a/partner_support/sample_customization/AndroidManifest.xml +++ b/partner_support/sample_customization/AndroidManifest.xml @@ -18,13 +18,13 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.tvcustomization"> <!-- Customization package must have this permission to customize TV apps. --> - <uses-permission android:name="com.android.tv.permission.CUSTOMIZE_TV_APP"/> + <uses-permission android:name="com.google.android.tv.permission.CUSTOMIZE_TV_APP"/> <!-- Enable leanback library support. --> <uses-feature android:name="android.software.leanback" android:required="true" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application android:label="Partner Customization" android:theme="@android:style/Theme.Holo.Light.NoActionBar" diff --git a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png Binary files differindex 26add7f6..454c515a 100755..100644 --- a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png +++ b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png diff --git a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png Binary files differindex 1ac20dbf..5e53eb55 100755..100644 --- a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png +++ b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png diff --git a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png Binary files differindex f6cf6450..898bac41 100755..100644 --- a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png +++ b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png diff --git a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png Binary files differindex 72a250db..9da29906 100755..100644 --- a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png +++ b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png diff --git a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png Binary files differindex 648001f7..ff5c4b1a 100755..100644 --- a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png +++ b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/partner_support/sample_customization/res/values/bools.xml b/partner_support/sample_customization/res/values/bools.xml index 54fbe071..259548b4 100644 --- a/partner_support/sample_customization/res/values/bools.xml +++ b/partner_support/sample_customization/res/values/bools.xml @@ -17,4 +17,6 @@ <resources> <bool name="tvprovider_allows_system_inserts_to_program_table">true</bool> + <bool name="tvprovider_allows_column_creation">true</bool> + <bool name="turn_off_embedded_tuner">true</bool> </resources>
\ No newline at end of file diff --git a/partner_support/samples/Android.mk b/partner_support/samples/Android.mk index 922a5b5d..2e771a5b 100644 --- a/partner_support/samples/Android.mk +++ b/partner_support/samples/Android.mk @@ -16,7 +16,7 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \ android-support-core-ui \ android-support-v7-recyclerview \ android-support-v17-leanback \ - android-support-tv-provider + androidx.tvprovider_tvprovider LOCAL_USE_AAPT2 := true diff --git a/partner_support/samples/AndroidManifest.xml b/partner_support/samples/AndroidManifest.xml index b9e086cb..d91c603a 100644 --- a/partner_support/samples/AndroidManifest.xml +++ b/partner_support/samples/AndroidManifest.xml @@ -29,12 +29,13 @@ <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <uses-permission android:name="com.android.tv.permission.RECEIVE_INPUT_EVENT" /> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <!--TODO(b/68949299): remove tool hint when we have smaller dependency targets--> <application android:label="@string/partner_support_sample_tv_input" - tools:replace="android:label,icon,theme" + tools:replace="android:label,icon,theme,appComponentFactory" android:icon="@mipmap/ic_launcher" - android:theme="@android:style/Theme.Holo.Light.NoActionBar" > + android:theme="@android:style/Theme.Holo.Light.NoActionBar" + android:appComponentFactory="android.support.v4.app.CoreComponentFactory" > <activity android:name=".SampleTvInputSetupActivity" android:theme="@style/Theme.Leanback.GuidedStep"> <intent-filter> diff --git a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png Binary files differindex a827add4..a044d2cc 100755..100644 --- a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png +++ b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png diff --git a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png Binary files differindex d7d36f26..26307c20 100755..100644 --- a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png +++ b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png diff --git a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png Binary files differindex 210bfcf1..49646832 100755..100644 --- a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png +++ b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png diff --git a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png Binary files differindex 59a090c7..93db5549 100755..100644 --- a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png +++ b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png diff --git a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png Binary files differindex 388b6ebe..cfc2fb11 100755..100644 --- a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png +++ b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java index 35f4b694..ec7589cb 100644 --- a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java +++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java @@ -23,8 +23,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.support.annotation.NonNull; -import android.support.media.tv.Channel; -import android.support.media.tv.TvContractCompat; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -32,6 +30,8 @@ import android.support.v17.leanback.widget.GuidedAction; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.tvprovider.media.tv.Channel; +import androidx.tvprovider.media.tv.TvContractCompat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java deleted file mode 100644 index aad51c76..00000000 --- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2018 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.google.android.tv.partner.support; - - - -/** - * Hand copy of generated Autovalue class. - * - * TODO get autovalue working - */ -final class AutoValue_EpgInput extends EpgInput { - - private final long id; - private final String inputId; - private final String lineupId; - - AutoValue_EpgInput( - long id, - String inputId, - String lineupId) { - this.id = id; - if (inputId == null) { - throw new NullPointerException("Null inputId"); - } - this.inputId = inputId; - if (lineupId == null) { - throw new NullPointerException("Null lineupId"); - } - this.lineupId = lineupId; - } - - @Override - public long getId() { - return id; - } - - @Override - public String getInputId() { - return inputId; - } - - @Override - public String getLineupId() { - return lineupId; - } - - @Override - public String toString() { - return "EpgInput{" - + "id=" + id + ", " - + "inputId=" + inputId + ", " - + "lineupId=" + lineupId - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof EpgInput) { - EpgInput that = (EpgInput) o; - return (this.id == that.getId()) - && (this.inputId.equals(that.getInputId())) - && (this.lineupId.equals(that.getLineupId())); - } - return false; - } - - @Override - public int hashCode() { - int h$ = 1; - h$ *= 1000003; - h$ ^= (int) ((id >>> 32) ^ id); - h$ *= 1000003; - h$ ^= inputId.hashCode(); - h$ *= 1000003; - h$ ^= lineupId.hashCode(); - return h$; - } - -} diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java deleted file mode 100644 index 076f8a2a..00000000 --- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2018 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.google.android.tv.partner.support; - -import android.support.annotation.Nullable; -import java.util.List; - -/** - * Hand copy of generated Autovalue class. - * - * TODO get autovalue working - */ - -final class AutoValue_Lineup extends Lineup { - - private final String id; - private final int type; - private final String name; - private final List<String> channels; - - AutoValue_Lineup( - String id, - int type, - @Nullable String name, - List<String> channels) { - if (id == null) { - throw new NullPointerException("Null id"); - } - this.id = id; - this.type = type; - this.name = name; - if (channels == null) { - throw new NullPointerException("Null channels"); - } - this.channels = channels; - } - - @Override - public String getId() { - return id; - } - - @Override - public int getType() { - return type; - } - - @Nullable - @Override - public String getName() { - return name; - } - - @Override - public List<String> getChannels() { - return channels; - } - - @Override - public String toString() { - return "Lineup{" - + "id=" + id + ", " - + "type=" + type + ", " - + "name=" + name + ", " - + "channels=" + channels - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof Lineup) { - Lineup that = (Lineup) o; - return (this.id.equals(that.getId())) - && (this.type == that.getType()) - && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName())) - && (this.channels.equals(that.getChannels())); - } - return false; - } - - @Override - public int hashCode() { - int h$ = 1; - h$ *= 1000003; - h$ ^= id.hashCode(); - h$ *= 1000003; - h$ ^= type; - h$ *= 1000003; - h$ ^= (name == null) ? 0 : name.hashCode(); - h$ *= 1000003; - h$ ^= channels.hashCode(); - return h$; - } - -} diff --git a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java index e40d90d7..1f7198eb 100644 --- a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java +++ b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java @@ -83,7 +83,14 @@ public class BaseCustomization { ? 0 : res.getIdentifier(resourceName, RES_TYPE_BOOLEAN, packageName); if (DEBUG) { - Log.d(TAG, "Boolean resource " + resourceName + " has " + resId); + Log.d( + TAG, + "Boolean resource " + + resourceName + + " has " + + resId + + " with value " + + (resId == 0 ? "missing" : res.getBoolean(resId))); } return resId == 0 ? Optional.empty() : Optional.of(res.getBoolean(resId)); } catch (PackageManager.NameNotFoundException e) { diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java index 82cc463a..20b3542a 100644 --- a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java +++ b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java @@ -17,13 +17,14 @@ package com.google.android.tv.partner.support; import android.content.ContentValues; +import com.google.auto.value.AutoValue; /** * Value class representing a TV Input that uses Live TV EPG. * * @see {@link EpgContract.EpgInputs} */ -// TODO(b/72052568): Get autovalue to work in aosp master +@AutoValue public abstract class EpgInput { public static EpgInput createEpgChannel(long id, String inputId, String lineupId) { diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java index 53485ec8..dddcd08c 100644 --- a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java +++ b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java @@ -59,6 +59,8 @@ public final class EpgInputs { result.add(EpgInput.createEpgChannel(contentValues)); } return result; + } catch (Exception e) { + return Collections.emptySet(); } } diff --git a/partner_support/src/com/google/android/tv/partner/support/Lineup.java b/partner_support/src/com/google/android/tv/partner/support/Lineup.java index 6123eebd..c5d30464 100644 --- a/partner_support/src/com/google/android/tv/partner/support/Lineup.java +++ b/partner_support/src/com/google/android/tv/partner/support/Lineup.java @@ -18,12 +18,13 @@ package com.google.android.tv.partner.support; import android.content.ContentValues; import android.support.annotation.Nullable; +import com.google.auto.value.AutoValue; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Value class for {@link com.google.android.tv.partner.support.EpgContract.Lineups} */ -// TODO(b/72052568): Get autovalue to work in aosp master +@AutoValue public abstract class Lineup { /** Lineup type for cable. */ public static final int LINEUP_CABLE = 0; diff --git a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java index 7ff168e1..11331076 100644 --- a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java +++ b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java @@ -32,6 +32,11 @@ public final class PartnerCustomizations extends BaseCustomization { public static final String TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE = "tvprovider_allows_system_inserts_to_program_table"; + public static final String TVPROVIDER_ALLOWS_COLUMN_CREATION = + "tvprovider_allows_column_creation"; + + public static final String TURN_OFF_EMBEDDED_TUNER = "turn_off_embedded_tuner"; + public PartnerCustomizations(Context context) { super(context, CUSTOMIZE_PERMISSIONS); } @@ -40,4 +45,12 @@ public final class PartnerCustomizations extends BaseCustomization { return getBooleanResource(context, TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE) .orElse(false); } + + public boolean doesTvProviderAllowColumnCreation(Context context) { + return getBooleanResource(context, TVPROVIDER_ALLOWS_COLUMN_CREATION).orElse(false); + } + + public boolean turnOffEmbeddedTuner(Context context) { + return getBooleanResource(context, TURN_OFF_EMBEDDED_TUNER).orElse(false); + } } diff --git a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java index a6292e3e..e2170589 100644 --- a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java +++ b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java @@ -25,12 +25,12 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import com.android.tv.testing.TestSingletonApp; import com.android.tv.testing.constants.ConfigConstants; -import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner; import java.util.ArrayList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -38,15 +38,8 @@ import org.robolectric.annotation.Config; // TODO: move to partner-support -@RunWith(GoogleRobolectricTestRunner.class) -@Config( - manifest = - "//third_party/java_src/android_app/live_channels/common/src" - + "/com/android/tv/common" - + ":common/AndroidManifest.xml", - sdk = ConfigConstants.SDK, - application = TestSingletonApp.class -) +@RunWith(RobolectricTestRunner.class) +@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class) public class BaseCustomizationTest { private static final String[] PERMISSIONS = {"com.example.permission"}; diff --git a/proguard.flags b/proguard.flags index 69b17861..b3795d65 100644 --- a/proguard.flags +++ b/proguard.flags @@ -68,3 +68,6 @@ # Grpc used by epg via reflection -keep class io.grpc.internal.DnsNameResolverProvider + +# Don't warn about checkerframework in Android proguard +-dontwarn org.checkerframework.** diff --git a/res/drawable-xhdpi/bg_protection.png b/res/drawable-xhdpi/bg_protection.png Binary files differdeleted file mode 100644 index 02df25ae..00000000 --- a/res/drawable-xhdpi/bg_protection.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_store.png b/res/drawable-xhdpi/ic_app_store.png Binary files differindex 767b8b64..767b8b64 100644 --- a/res/drawable-xhdpi/ic_store.png +++ b/res/drawable-xhdpi/ic_app_store.png diff --git a/res/drawable-xhdpi/ic_check_circle_white_48dp.png b/res/drawable-xhdpi/ic_check_circle_white_48dp.png Binary files differdeleted file mode 100644 index a1cf83e6..00000000 --- a/res/drawable-xhdpi/ic_check_circle_white_48dp.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png b/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png Binary files differdeleted file mode 100644 index 594af851..00000000 --- a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_error_outline_pink_24dp.png b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png Binary files differnew file mode 100644 index 00000000..d48becee --- /dev/null +++ b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png diff --git a/res/drawable-xhdpi/ic_error_white_48dp.png b/res/drawable-xhdpi/ic_error_white_48dp.png Binary files differdeleted file mode 100644 index 8c2cf1e5..00000000 --- a/res/drawable-xhdpi/ic_error_white_48dp.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_live_channels.png b/res/drawable-xhdpi/ic_live_channels.png Binary files differdeleted file mode 100644 index bb1c2d9d..00000000 --- a/res/drawable-xhdpi/ic_live_channels.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_tv_app.png b/res/drawable-xhdpi/ic_tv_app.png Binary files differnew file mode 100644 index 00000000..c061bf04 --- /dev/null +++ b/res/drawable-xhdpi/ic_tv_app.png diff --git a/res/drawable-xhdpi/ic_live_channels_96x96.png b/res/drawable-xhdpi/ic_tv_app_96x96.png Binary files differindex 7f4eb10d..7f4eb10d 100644 --- a/res/drawable-xhdpi/ic_live_channels_96x96.png +++ b/res/drawable-xhdpi/ic_tv_app_96x96.png diff --git a/res/drawable-xhdpi/ic_warning_white_18dp.png b/res/drawable-xhdpi/ic_warning_white_18dp.png Binary files differdeleted file mode 100644 index 13d573e1..00000000 --- a/res/drawable-xhdpi/ic_warning_white_18dp.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_warning_white_96dp.png b/res/drawable-xhdpi/ic_warning_white_96dp.png Binary files differdeleted file mode 100644 index 50d1f295..00000000 --- a/res/drawable-xhdpi/ic_warning_white_96dp.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_warning_yellow_24dp.png b/res/drawable-xhdpi/ic_warning_yellow_24dp.png Binary files differnew file mode 100644 index 00000000..accb0613 --- /dev/null +++ b/res/drawable-xhdpi/ic_warning_yellow_24dp.png diff --git a/res/drawable-xhdpi/banner.png b/res/drawable-xhdpi/live_tv_banner.png Binary files differindex 57dbb05e..57dbb05e 100644 --- a/res/drawable-xhdpi/banner.png +++ b/res/drawable-xhdpi/live_tv_banner.png diff --git a/res/drawable-xhdpi/usb_antenna.png b/res/drawable-xhdpi/usb_antenna.png Binary files differdeleted file mode 100644 index ca5b2d72..00000000 --- a/res/drawable-xhdpi/usb_antenna.png +++ /dev/null diff --git a/res/drawable/menu_background.xml b/res/drawable/menu_background.xml index bdd55c8c..defee88f 100644 --- a/res/drawable/menu_background.xml +++ b/res/drawable/menu_background.xml @@ -14,8 +14,13 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - -<bitmap xmlns:android="http://schemas.android.com/apk/res/android" - android:src="@drawable/bg_protection" - android:tileModeX="repeat" - android:tileModeY="mirror" /> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <!-- 0% to 20%black half way down then 100% black--> + <gradient + android:startColor="#00000000" + android:endColor="#000000" + android:centerColor="#a0000000" + android:centerY=".5" + android:angle="270" /> +</shape> diff --git a/res/layout/channel_banner.xml b/res/layout/channel_banner.xml index 3f105fe3..4d3cc244 100644 --- a/res/layout/channel_banner.xml +++ b/res/layout/channel_banner.xml @@ -83,13 +83,22 @@ android:layout_toEndOf="@id/anchor" android:visibility="gone" /> + <ImageView android:id="@+id/channel_signal_strength" + android:layout_width="@dimen/channel_banner_input_logo_size" + android:layout_height="@dimen/channel_banner_input_logo_size" + android:layout_marginEnd="8dp" + android:layout_marginBottom="-2dp" + android:layout_alignBottom="@id/anchor" + android:layout_toEndOf="@id/tvinput_logo" + android:visibility="gone" /> + <TextView android:id="@+id/channel_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="12dp" android:layout_marginBottom="-4sp" android:layout_alignBottom="@id/anchor" - android:layout_toEndOf="@id/tvinput_logo" + android:layout_toEndOf="@id/channel_signal_strength" android:singleLine="true" android:ellipsize="end" android:maxWidth="@dimen/channel_name_max_width" diff --git a/res/layout/dvr_details_description.xml b/res/layout/dvr_details_description.xml index d55688ba..ee749526 100644 --- a/res/layout/dvr_details_description.xml +++ b/res/layout/dvr_details_description.xml @@ -42,6 +42,27 @@ android:orientation="vertical" android:background="?android:attr/selectableItemBackground"> + <LinearLayout android:id="@+id/dvr_details_description_error_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:visibility="gone"> + + <ImageView android:layout_width="30dp" + android:layout_height="27dp" + android:paddingTop="9dp" + android:paddingLeft="12dp" + android:src="@drawable/ic_error_outline_pink_24dp"/> + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="9dp" + android:paddingRight="12dp" + android:textColor="@color/dvr_recording_failed_text_color" + android:text="@string/dvr_recording_failed" + style="?attr/detailsDescriptionBodyStyle" /> + </LinearLayout> + <TextView android:id="@+id/dvr_details_description_body" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/res/layout/dvr_recording_card_view.xml b/res/layout/dvr_recording_card_view.xml index 3e953510..3bf9bf6f 100644 --- a/res/layout/dvr_recording_card_view.xml +++ b/res/layout/dvr_recording_card_view.xml @@ -30,7 +30,7 @@ android:layout_gravity="center_horizontal" android:scaleType="centerCrop" android:contentDescription="@null" - tv:layout_viewType="main" /> + tv:layout_viewType="main"/> <ProgressBar android:id="@+id/recording_progress" style="@android:style/Widget.ProgressBar.Horizontal" @@ -42,21 +42,7 @@ android:indeterminate="false" android:visibility="gone" android:max="100" - android:layout_gravity="bottom" /> - - <FrameLayout android:id="@+id/affiliated_icon_container" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/card_image_gradient" - android:visibility="invisible"> - - <ImageView android:id="@+id/affiliated_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom|right" - android:layout_margin="12dp" /> - - </FrameLayout> + android:layout_gravity="bottom"/> </FrameLayout> <LinearLayout android:id="@+id/info_area" @@ -99,10 +85,17 @@ android:layout_width="match_parent" android:layout_height="wrap_content" > + <ImageView android:id="@+id/content_icon" + android:paddingTop="2dp" + android:layout_width="13dp" + android:layout_height="15dp" + android:gravity="start" + android:visibility="gone"/> + <TextView android:id="@+id/content_major" + android:layout_toEndOf="@+id/content_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="start" style="@style/dvr_card_view_content_text" /> <TextView android:id="@+id/content_minor" diff --git a/res/layout/dvr_schedules_item.xml b/res/layout/dvr_schedules_item.xml index 90e1123d..9e9ee6af 100644 --- a/res/layout/dvr_schedules_item.xml +++ b/res/layout/dvr_schedules_item.xml @@ -100,14 +100,25 @@ android:lines="1" android:textColor="@color/dvr_schedules_item_info"/> </LinearLayout> - <TextView android:id="@+id/conflict_info" - android:layout_width="match_parent" + + <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="start" - android:textSize="10sp" - android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin" - android:textColor="@color/dvr_schedules_item_info" - android:visibility="gone"/> + android:orientation="horizontal"> + <ImageView android:id="@+id/extra_info_icon" + android:layout_width="13dp" + android:layout_height="13dp" + android:paddingTop="2dp" + android:src="@drawable/ic_error_outline_pink_24dp" + android:visibility="gone"/> + <TextView android:id="@+id/extra_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start" + android:textSize="10sp" + android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin" + android:textColor="@color/dvr_schedules_item_info" + android:visibility="gone"/> + </LinearLayout> </LinearLayout> </LinearLayout> diff --git a/res/layout/menu_card_down.xml b/res/layout/menu_card_down.xml new file mode 100644 index 00000000..0ccfc897 --- /dev/null +++ b/res/layout/menu_card_down.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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. + --> + +<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/card_layout_width" + android:layout_height="wrap_content" + android:orientation="vertical" + android:elevation="@dimen/card_elevation_normal" + android:focusable="true" + android:clickable="true"> + + <ImageView + android:layout_width="@dimen/card_image_layout_width" + android:layout_height="@dimen/card_image_layout_height" + android:background="@color/channel_card_guide" + android:paddingBottom="16dp" + android:paddingEnd="30dp" + android:paddingStart="30dp" + android:paddingTop="16dp" + android:src="@drawable/quantum_ic_arrow_downward_white_36" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="@dimen/card_meta_layout_height" + android:paddingStart="@dimen/card_meta_padding_start" + android:paddingEnd="@dimen/card_meta_padding_end" + android:paddingTop="@dimen/card_meta_padding_top" + android:singleLine="true" + android:ellipsize="end" + android:fontFamily="@string/condensed_font" + android:textColor="@color/card_meta_text_color" + android:background="@color/guide_card_meta_background" + android:text="@string/channels_item_down" + android:textSize="12sp" /> + +</com.android.tv.menu.SimpleCardView> diff --git a/res/layout/menu_card_up.xml b/res/layout/menu_card_up.xml new file mode 100644 index 00000000..2ba365ef --- /dev/null +++ b/res/layout/menu_card_up.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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. + --> + +<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/card_layout_width" + android:layout_height="wrap_content" + android:orientation="vertical" + android:elevation="@dimen/card_elevation_normal" + android:focusable="true" + android:clickable="true"> + + <ImageView + android:layout_width="@dimen/card_image_layout_width" + android:layout_height="@dimen/card_image_layout_height" + android:background="@color/channel_card_guide" + android:paddingBottom="16dp" + android:paddingEnd="30dp" + android:paddingStart="30dp" + android:paddingTop="16dp" + android:src="@drawable/quantum_ic_arrow_upward_white_36" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="@dimen/card_meta_layout_height" + android:paddingStart="@dimen/card_meta_padding_start" + android:paddingEnd="@dimen/card_meta_padding_end" + android:paddingTop="@dimen/card_meta_padding_top" + android:singleLine="true" + android:ellipsize="end" + android:fontFamily="@string/condensed_font" + android:textColor="@color/card_meta_text_color" + android:background="@color/guide_card_meta_background" + android:text="@string/channels_item_up" + android:textSize="12sp" /> + +</com.android.tv.menu.SimpleCardView> diff --git a/res/layout/pin_dialog.xml b/res/layout/pin_dialog.xml index 5071717d..d40d70ec 100644 --- a/res/layout/pin_dialog.xml +++ b/res/layout/pin_dialog.xml @@ -35,7 +35,8 @@ android:textColor="@color/pin_dialog_text_color" android:fontFamily="@string/font" android:visibility="invisible" - android:singleLine="false"/> + android:singleLine="false" + android:focusableInTouchMode="true"/> <LinearLayout android:id="@+id/enter_pin" @@ -54,36 +55,14 @@ android:fontFamily="@string/font" android:singleLine="false" /> - <LinearLayout + <com.android.tv.dialog.picker.PinPicker + android:id="@+id/pin_picker" + android:importantForAccessibility="yes" android:layout_width="match_parent" - android:layout_height="144dp" + android:layout_height="154dp" android:paddingStart="24dp" android:paddingEnd="24dp" android:gravity="center" - android:orientation="horizontal"> - - <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker" - android:id="@+id/first" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker" - android:id="@+id/second" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" /> - - <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker" - android:id="@+id/third" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" /> - - <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker" - android:id="@+id/fourth" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" /> - </LinearLayout> + /> </LinearLayout> </FrameLayout> diff --git a/res/layout/pin_number_picker.xml b/res/layout/pin_number_picker.xml deleted file mode 100644 index 8e8de9f5..00000000 --- a/res/layout/pin_number_picker.xml +++ /dev/null @@ -1,51 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2015 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="48dp" - android:layout_height="144dp"> - - <TextView android:id="@+id/focused_background" - android:layout_width="@dimen/pin_number_picker_text_view_width" - android:layout_height="@dimen/pin_number_picker_text_view_height" - android:layout_gravity="center" - android:gravity="center" - android:textSize="@dimen/pin_number_picker_text_size" - android:textColor="@color/pin_number_picker_text_color" - android:fontFamily="@string/light_font" - android:background="@drawable/pin_number_picker_focused_background" /> - - <LinearLayout - android:id="@+id/number_view_holder" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:focusable="true" - android:orientation="vertical"> - - <TextView android:id="@+id/previous2_number" - style="@style/pin_number_view"/> - <TextView android:id="@+id/previous_number" - style="@style/pin_number_view"/> - <TextView android:id="@+id/current_number" - style="@style/pin_number_view"/> - <TextView android:id="@+id/next_number" - style="@style/pin_number_view"/> - <TextView android:id="@+id/next2_number" - style="@style/pin_number_view"/> - </LinearLayout> - -</FrameLayout> diff --git a/res/layout/tunable_tv_view.xml b/res/layout/tunable_tv_view.xml index 549d0535..00c9908c 100644 --- a/res/layout/tunable_tv_view.xml +++ b/res/layout/tunable_tv_view.xml @@ -17,27 +17,6 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" > - <View android:id="@+id/channel_up" - android:layout_width="wrap_content" - android:focusable="false" - android:focusableInTouchMode="true" - android:layout_height="1dp" - android:layout_gravity="top" /> - <View android:id="@+id/placeholder" - android:layout_width="1dp" - android:layout_height="1dp" - android:focusable="false" - android:focusableInTouchMode="true" - android:focusedByDefault="true" - android:layout_gravity="center" /> - - <View android:id="@+id/channel_down" - android:layout_width="wrap_content" - android:focusable="false" - android:focusableInTouchMode="true" - android:layout_height="1dp" - android:layout_gravity="bottom" /> - <com.android.tv.ui.AppLayerTvView android:id="@+id/tv_view" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/res/values/arrays.xml b/res/values/arrays.xml index e57b0546..0604dd2b 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -29,31 +29,130 @@ <!-- The category strings to be displayed in the channel guide. This list should be synced the data in src/com/android/tv/data/GenreItems.java --> <eat-comment /> - <!-- Genre list [CHAR LIMIT=20] --> + <!-- Genre list [CHAR LIMIT=25] --> <string-array name="genre_labels" translatable="true"> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "All channels", implies all channels will be shown in the program guide. + [CHAR LIMIT=25] --> <item>All channels</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Family/Kids", implies only channels with "Family/Kids" programs will be shown in the + guide. + "Family/Kids" programs, are programs designed for families and safe for viewing by + young children. + [CHAR LIMIT=25] --> <item>Family/Kids</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Sports", implies only channels with "Sports" programs will be shown in the guide. + "Sports" programs, include sporting events, news and other shows about sports. + [CHAR LIMIT=25] --> <item>Sports</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Shopping", implies only channels with "Shopping" programs will be shown in the guide. + "Shopping" programs are TV shows where people can buy or bid on items. + [CHAR LIMIT=25] --> <item>Shopping</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Movies", implies only channels with "Movies" programs will be shown in the guide. + [CHAR LIMIT=25] --> <item>Movies</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Comedy", implies only channels with "Comedy" programs will be shown in the guide. + "Comedy" programs are generally intended to be humorous or amusing. + [CHAR LIMIT=25] --> <item>Comedy</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Travel", implies only channels with "Travel" programs will be shown in the guide. + "Travel" programs feature popular destination or travel reviews. + [CHAR LIMIT=25] --> <item>Travel</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Drama", implies only channels with "Drama" programs will be shown in the guide. + "Drama" programs are fictional shows, featuring actors. + [CHAR LIMIT=25] --> <item>Drama</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Education", implies only channels with "Education" programs will be shown in the guide. + "Education" programs are designed to teach, either formally or informally. + [CHAR LIMIT=25] --> <item>Education</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Animal/Wildlife", implies only channels with "Animal/Wildlife" programs will be shown in + the guide. + "Animal/Wildlife" programs are about wild animals or pets. + [CHAR LIMIT=25] --> <item>Animal/Wildlife</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "News", implies only channels with "News" programs will be shown in the guide. + "News" programs report world or local events as they unfold. + [CHAR LIMIT=25] --> <item>News</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Gaming", implies only channels with "Gaming" programs will be shown in the guide. + "Gaming" programs rare about games, including video games and board games, + but excluding sports. + [CHAR LIMIT=25] --> <item>Gaming</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Arts", implies only channels with "Arts" programs will be shown in the guide. + "Arts" programs about artistic endeavours or events like dance, music theater, drawing. + [CHAR LIMIT=25] --> <item>Arts</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Entertainment", implies only channels with "Entertainment" programs will be shown in + the guide. + "Entertainment" programs discuss news and the people involved in the entertainment + industry. + [CHAR LIMIT=25] --> <item>Entertainment</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Lifestyle", implies only channels with "Lifestyle" programs will be shown in + the guide. + "Lifestyle" programs feature topics such as fashion, diet, exercise, health and leisure + pursuits. + [CHAR LIMIT=25] --> <item>Lifestyle</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Music", implies only channels with "Music" programs will be shown in + the guide. + "Music" programs feature live or recorded music. + [CHAR LIMIT=25] --> <item>Music</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Premier", implies only channels with "Premier" programs will be shown in + the guide. + "Premier" programs are available at an extra cost. + [CHAR LIMIT=25] --> <item>Premier</item> + <!-- This is an item in a list to filter channels shown in a TV program guide, based on + genres. + "Tech/Science", implies only channels with "Tech/Science" programs will be shown in + the guide. + "Tech/Science" programs are about science or technology . + [CHAR LIMIT=25] --> <item>Tech/Science</item> </string-array> <!-- Titles in the onboarding page. --> <string-array name="welcome_page_titles"> - <item>Live TV</item> + <item><xliff:g id="app_name">Live TV</xliff:g> </item> <item>A simple way to discover content</item> <item>Download apps, get more channels</item> <item>Customize your channel line-up</item> diff --git a/res/values/colors.xml b/res/values/colors.xml index e0a0b99f..b68feb13 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -158,4 +158,6 @@ <color name="dvr_guided_step_action_text_color_selected">#111111</color> <color name="dvr_detail_default_background">#FF01579B</color> <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> </resources> diff --git a/tuner/tests/TestManifest.xml b/res/values/strings-custom.xml index f84aa90f..22f73318 100644 --- a/tuner/tests/TestManifest.xml +++ b/res/values/strings-custom.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2018 The Android Open Source Project + ~ Copyright (C) 2019 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. @@ -14,10 +14,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> +<resources> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.tv.tuner.tests"> - <!-- android_local_test needs minSdkVersion set --> - <uses-sdk android:minSdkVersion="23"/> + <!-- Name of application [CHAR LIMIT=NONE] --> + <string name="app_name" translatable="false">Live TV</string> -</manifest>
\ No newline at end of file +</resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index cea4ee6c..36824759 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -26,21 +26,18 @@ <string name="option_item_divider_font" translatable="false">@string/condensed_font</string> - <!-- Name of application [CHAR LIMIT=NONE] --> - <string name="app_name">Live TV</string> - <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permlab_receiveInputEvent" translatable="false">receive input events from Live TV app</string> + <string name="permlab_receiveInputEvent" translatable="false">receive input events from <xliff:g id="app_name">Live TV</xliff:g> app</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from Live TV app</string> + <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from <xliff:g id="app_name">Live TV</xliff:g> app</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permlab_customizeTvApp" translatable="false">customize Live TV app</string> + <string name="permlab_customizeTvApp" translatable="false">customize <xliff:g id="app_name">Live TV</xliff:g> app</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize Live TV app</string> + <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize <xliff:g id="app_name">Live TV</xliff:g> app</string> <!-- Program information, mainly used for channel banner and program guide. --> <eat-comment /> @@ -90,6 +87,12 @@ <!-- Label of Program guide item in the channel list row. [CHAR LIMIT=23] --> <string name="channels_item_program_guide">Program guide</string> + <!-- Label of the item in the "channel menu" that changes the current TV channel in the "up" + direction, to the next larger channel number. [CHAR LIMIT=23] --> + <string name="channels_item_up">Channel up</string> + <!-- Label of the item in the "channel menu" that changes the current TV channel in the "down" + direction, to the next smaller channel number. [CHAR LIMIT=23] --> + <string name="channels_item_down">Channel down</string> <!-- Label of setup item in the channel list row. The item is shown only when new inputs are installed. [CHAR LIMIT=23] --> <string name="channels_item_setup">New channels available</string> @@ -366,10 +369,22 @@ <eat-comment /> <!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] --> <string name="tvview_channel_locked">To watch this channel, press Right and enter your PIN</string> + <!-- Description on the locked screen when current channel is locked by parental control and talk back is turned on. + "press select" refers to a button on the remote. [CHAR LIMIT=NONE] --> + <string name="tvview_channel_locked_talkback">To watch this channel, press select and enter your PIN</string> <!-- Description on the locked screen when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] --> <string name="tvview_content_locked">To watch this program, press Right and enter your PIN</string> + <!-- Description on the locked screen when the rating of the current content is restricted by parental control and talk back is turned on. + "press select" refers to a button on the remote.[CHAR LIMIT=NONE] --> + <string name="tvview_content_locked_talkback">To watch this program, press select and enter your PIN</string> <!-- Description on the locked screen when the current content is unrated and it's restricted by parental control. [CHAR LIMIT=NONE] --> <string name="tvview_content_locked_unrated">This program is unrated.\nTo watch this program, press Right and enter your PIN</string> + <!-- Description on the locked screen when the current content is unrated and it's restricted by parental control and talk back is turned on. + "press select" refers to a button on the remote.[CHAR LIMIT=NONE] --> + <string name="tvview_content_locked_unrated_talkback">This program is unrated.\nTo watch this program, press select and enter your PIN</string> + <!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control and talk back is turned on. [CHAR LIMIT=NONE] + "press select" refers to a button on the remote.--> + <string name="tvview_content_locked_format_talkback">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press select and enter your PIN.</string> <!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] --> <string name="tvview_content_locked_format">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press Right and enter your PIN.</string> <!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] --> @@ -464,7 +479,7 @@ the source video through to the display and don't provide ability to tune to a specific channel unless the user directly controls the external source device (e.g. game console, DVD player, settop box, etc) that is connected to the TV. [CHAR LIMIT=NONE] --> - <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch Live TV app for tuner type TV input.</string> + <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch <xliff:g id="app_name">Live TV</xliff:g> app for tuner type TV input.</string> <!-- Error message when tune is failed. [CHAR LIMIT=NONE] --> <string name="msg_tune_failed">Tune failed</string> <!-- Error message when the user attempts an action (select TIS setup-activity, app-link, @@ -475,11 +490,13 @@ <string name="msg_all_channels_hidden">All source channels are hidden.\nSelect at least one channel to watch.</string> <!-- Message displayed when availability is changed by unknown reason. [CHAR LIMIT=NONE] --> <string name="msg_channel_unavailable_unknown">The video is unexpectedly unavailable</string> + <!-- Message displayed when a TV input (eg HDMI Cable) is not physically connected. [CHAR LIMIT=NONE] --> + <string name="msg_channel_unavailable_not_connected">No Signal. Please check your source connection.</string> <!-- Message to notify the different use of Back Button: Home Button(To exit) Back button (commands for external device) [CHAR LIMIT=NONE] --> <string name="msg_back_key_guide">BACK key is for connected device. Press HOME button to exit.</string> <!-- Error message when a user denied to grant READ_TV_LISTING permission. [CHAR LIMIT=NONE] --> - <string name="msg_read_tv_listing_permission_denied">Live TV needs permission to read the TV listings.</string> + <string name="msg_read_tv_listing_permission_denied"><xliff:g id="app_name">Live TV</xliff:g> needs permission to read the TV listings.</string> <!-- Strings for debug or not to be shown to users --> <eat-comment /> @@ -505,13 +522,13 @@ <string name="dvr_history_dialog_title" translatable="false">DVR history</string> <!-- Display name of DVR recording service's notification channel. --> - <string name="dvr_notification_channel_name" translatable="false">Live TV DVR</string> + <string name="dvr_notification_channel_name" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> DVR</string> <!-- Content title of DVR recording service's notification. --> - <string name="dvr_notification_content_title" translatable="false">Live TV DVR</string> + <string name="dvr_notification_content_title" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> DVR</string> <!-- Content text of DVR recording service's notification during recording. --> - <string name="dvr_notification_content_text_recording" translatable="false">Live TV are recording.</string> + <string name="dvr_notification_content_text_recording" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> are recording.</string> <!-- Content text of DVR recording service's notification during updating schedules. --> - <string name="dvr_notification_content_text_loading" translatable="false">Live TV are updating recording schedules.</string> + <string name="dvr_notification_content_text_loading" translatable="false"><xliff:g id="app_name">Live TV</xliff:g> are updating recording schedules.</string> <!-- Default content title of tuner installing notifications. --> <string name="tuner_install_notification_content_title" translatable="false">Install <xliff:g id="tuner_package" example="Tuner package">%s</xliff:g></string> @@ -577,7 +594,11 @@ <!-- Description of a card view to show full list of scheduled recordings. [CHAR LIMIT=25] --> <string name="dvr_full_schedule_card_view_title">Full schedule</string> <!-- Description of failed recordings. [CHAR LIMIT=25] --> - <string name="dvr_recording_failed">Recording Failed</string> + <string name="dvr_recording_failed">Recording Failed.</string> + <!-- Description of failed recordings. [CHAR LIMIT=25] --> + <string name="dvr_recording_failed_no_period">Recording Failed</string> + <!-- Description of recording conflicts. [CHAR LIMIT=25] --> + <string name="dvr_recording_conflict">Recording Conflict</string> <!-- Description of how many following days the schedule list will show. [CHAR LIMIT=25] --> <plurals name="dvr_full_schedule_card_view_content"> <item quantity="one">Next %1$d day</item> @@ -607,6 +628,8 @@ <!-- DVR detailed page --> <eat-comment /> + <!-- Button label to schedule a recording. --> + <string name="dvr_detail_schedule_recording">Schedule recording</string> <!-- Button label to cancel the recording schedule. --> <string name="dvr_detail_cancel_recording">Cancel recording</string> <!-- Button label to stop the current recording. --> @@ -631,6 +654,18 @@ <string name="dvr_detail_view_schedule">View schedule</string> <!-- Text label to indicate there's more text in the details description [CHAR LIMIT=20] --> <string name="dvr_detail_read_more">Read more</string> + <!-- Description of failed recordings caused by system failures. --> + <string name="dvr_recording_failed_system_failure">System failure. (Error code: <xliff:g id="errorCode" example="0">%1$d</xliff:g>)</string> + <!-- Description of failed recordings when they are not started correctly. --> + <string name="dvr_recording_failed_not_started">Recording was not started. Please check the antenna and hard drive connections (if any).</string> + <!-- Description of failed recordings when required resource is busy. --> + <string name="dvr_recording_failed_resource_busy">Failed to tune to the channel. Possible causes: weak signal, poor antenna connection, or recording conflict.</string> + <!-- Description of failed recordings when the input is unavailable. --> + <string name="dvr_recording_failed_input_unavailable"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string> + <!-- Description of failed recordings when the input doesn't support recording. --> + <string name="dvr_recording_failed_input_dvr_unsupported">Recording of this channel is not supported.</string> + <!-- Description of failed recordings when the space is insufficient. --> + <string name="dvr_recording_failed_insufficient_space">Insufficient space. Please connect an external storage device or delete some existing recordings.</string> <!-- DVR series settings --> @@ -754,7 +789,7 @@ sufficient space.--> <string name="dvr_error_insufficient_space_description_three_or_more_recordings">The recordings of <xliff:g id="programName_1" example="Friends">%1$s</xliff:g>, <xliff:g id="programName_2" example="Friends">%2$s</xliff:g> and <xliff:g id="programName_3" example="Friends">%3$s</xliff:g> didn\'t complete due to insufficient storage.</string> <!-- Dialog title which will be shown when the current storage is too small for DVR. --> - <string name="dvr_error_small_sized_storage_title">More stroage needed</string> + <string name="dvr_error_small_sized_storage_title">More storage needed</string> <!-- Dialog description which will be shown when the current storage is too small for DVR. --> <string name="dvr_error_small_sized_storage_description">You will be able to record programs. However there is not enough storage on your device to start recording. Please connect an external drive that is <xliff:g id="storage_size" example="10GB">%1$d</xliff:g>GB or larger and follow the steps to format it as device storage.</string> <!-- Dialog title which will be shown when there is no free space on the current storage for DVR. --> @@ -930,6 +965,20 @@ <item quantity="other">(%1$d minutes)</item> </plurals> + <!-- DVR history list strings --> + <!-- Short description of failed recordings. --> + <string name="dvr_recording_failed_short">Failed.</string> + <!-- Short description of failed recordings when they are not started correctly. --> + <string name="dvr_recording_failed_not_started_short">Failed to start recording.</string> + <!-- Short description of failed recordings when required resource is busy. --> + <string name="dvr_recording_failed_resource_busy_short">Failed to tune to the channel.</string> + <!-- Short description of failed recordings when the input is unavailable. --> + <string name="dvr_recording_failed_input_unavailable_short"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string> + <!-- Short description of failed recordings when the input doesn't support recording. --> + <string name="dvr_recording_failed_input_dvr_unsupported_short">Recording not supported.</string> + <!-- Short description of failed recordings when the space is insufficient. --> + <string name="dvr_recording_failed_insufficient_space_short">Insufficient space.</string> + <!-- DVR date related strings --> <eat-comment/> <!-- Date text to represent today. --> @@ -952,4 +1001,21 @@ <eat-comment/> <!-- Name for recorded programs preview channel --> <string name="recorded_programs_preview_channel">Recorded Programs</string> + + <!-- Request Permission --> + <eat-comment /> + <!-- Title of the dialog to show storage permission rationale --> + <string name="write_storage_permission_rationale_title">Request permission</string> + <!-- The users has asked to delete TV programs they have recorded, however the application + first needs a system permission to delete files. + This is the text of a dialog that explains to the users they will be asked to grant + Live TV permission to delete files. + "Allow Live TV to access photos, media, and files on your device?\" is from the text + from the system message at + https://tc.corp.google.com/btviewer/messagedetail?project=AndroidPlatform&msgId=7885942926944299560 + --> + <string name="write_storage_permission_rationale_description">You will be asked to, \"Allow <xliff:g id="app_name">Live TV</xliff:g> to access photos, media, and files on your device?\"\n + 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> </resources> diff --git a/res/values/themes.xml b/res/values/themes.xml index 2165a756..96537077 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -77,7 +77,6 @@ <item name="guidanceIconStyle">@style/TV.Dvr.GuidanceIconStyle</item> <item name="guidedActionsListStyle">@style/TV.Dvr.GuidedActionsListStyle</item> <item name="guidedActionItemContainerStyle">@style/TV.Dvr.GuidedActionItemContainerStyle</item> - <item name="guidedActionContentWidthWeight">@string/lb_guidedactions_width_weight</item> </style> <style name="Theme.TV.Dvr.GuidedStep.Twoline.Action" parent = "Theme.TV.Dvr.GuidedStep"> @@ -120,4 +119,4 @@ clicking DVR cards overlapping with fragment transition. --> <item name="android:windowAllowEnterTransitionOverlap">false</item> </style> -</resources>
\ No newline at end of file +</resources> diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..6d5cb547 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +include ':common' +include ':tuner' +include ':SampleDvbTuner' +project(":SampleDvbTuner").projectDir = file("tuner/SampleDvbTuner") diff --git a/src/com/android/tv/ChannelChanger.java b/src/com/android/tv/ChannelChanger.java new file mode 100644 index 00000000..55035696 --- /dev/null +++ b/src/com/android/tv/ChannelChanger.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 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; + +/** Changes the channel. */ +public interface ChannelChanger { + + void channelUp(); + + void channelDown(); +} diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index 8ab145a4..fe138980 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -97,13 +97,7 @@ public class ChannelTuner { mStarted = true; mChannelDataManager.addListener(mChannelDataManagerListener); if (mChannelDataManager.isDbLoadFinished()) { - mHandler.post( - new Runnable() { - @Override - public void run() { - mChannelDataManagerListener.onLoadFinished(); - } - }); + mHandler.post(mChannelDataManagerListener::onLoadFinished); } } diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index 4f298ed6..ea17751b 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -20,11 +20,8 @@ import android.annotation.TargetApi; import android.content.Context; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; -import android.media.tv.TvRecordingClient; -import android.media.tv.TvRecordingClient.RecordingCallback; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; -import android.media.tv.TvView.TvInputCallback; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -36,9 +33,15 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import com.android.tv.common.compat.TvRecordingClientCompat; +import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat; +import com.android.tv.common.compat.TvViewCompat; +import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; import com.android.tv.data.api.Channel; +import com.android.tv.dvr.DvrTvView; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; +import com.android.tv.ui.api.TunableTvViewPlayingApi; import com.android.tv.util.TvInputManagerHelper; import java.util.Collections; import java.util.List; @@ -87,7 +90,9 @@ public class InputSessionManager { @MainThread @NonNull public TvViewSession createTvViewSession( - TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) { + TvViewCompat tvView, + TunableTvViewPlayingApi tunableTvView, + TvInputCallbackCompat callback) { TvViewSession session = new TvViewSession(tvView, tunableTvView, callback); mTvViewSessions.add(session); if (DEBUG) Log.d(TAG, "TvView session created: " + session); @@ -107,7 +112,7 @@ public class InputSessionManager { public RecordingSession createRecordingSession( String inputId, String tag, - RecordingCallback callback, + RecordingCallbackCompat callback, Handler handler, long endTimeMs) { RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); @@ -237,9 +242,10 @@ public class InputSessionManager { */ @MainThread public class TvViewSession { - private final TvView mTvView; - private final TunableTvView mTunableTvView; - private final TvInputCallback mCallback; + private final TvViewCompat mTvView; + private final TunableTvViewPlayingApi mTunableTvView; + private final TvInputCallbackCompat mCallback; + private final boolean mIsDvrSession; private Channel mChannel; private String mInputId; private Uri mChannelUri; @@ -248,10 +254,14 @@ public class InputSessionManager { private boolean mTuned; private boolean mNeedToBeRetuned; - TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) { + TvViewSession( + TvViewCompat tvView, + TunableTvViewPlayingApi tunableTvView, + TvInputCallbackCompat callback) { mTvView = tvView; mTunableTvView = tunableTvView; mCallback = callback; + mIsDvrSession = tunableTvView instanceof DvrTvView; mTvView.setCallback( new DelegateTvInputCallback(mCallback) { @Override @@ -338,9 +348,13 @@ public class InputSessionManager { void retune() { if (DEBUG) Log.d(TAG, "Retune requested."); + if (mIsDvrSession) { + Log.w(TAG, "DVR session should not call retune()!"); + return; + } if (mNeedToBeRetuned) { if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}"); - mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener); + ((TunableTvView) mTunableTvView).tuneTo(mChannel, mParams, mOnTuneListener); mNeedToBeRetuned = false; } } @@ -369,9 +383,13 @@ public class InputSessionManager { void resetByRecording() { mCallback.onVideoUnavailable( mInputId, TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE); + if (mIsDvrSession) { + Log.w(TAG, "DVR session should not call resetByRecording()!"); + return; + } if (mTuned) { if (DEBUG) Log.d(TAG, "Reset TvView session by recording"); - mTunableTvView.resetByRecording(); + ((TunableTvView) mTunableTvView).resetByRecording(); reset(); } mNeedToBeRetuned = true; @@ -386,22 +404,22 @@ public class InputSessionManager { public class RecordingSession { private final String mInputId; private Uri mChannelUri; - private final RecordingCallback mCallback; + private final RecordingCallbackCompat mCallback; private final Handler mHandler; private volatile long mEndTimeMs; - private TvRecordingClient mClient; + private TvRecordingClientCompat mClient; private boolean mTuned; RecordingSession( String inputId, String tag, - RecordingCallback callback, + RecordingCallbackCompat callback, Handler handler, long endTimeMs) { mInputId = inputId; mCallback = callback; mHandler = handler; - mClient = new TvRecordingClient(mContext, tag, callback, handler); + mClient = new TvRecordingClientCompat(mContext, tag, callback, handler); mEndTimeMs = endTimeMs; } @@ -409,29 +427,26 @@ public class InputSessionManager { if (DEBUG) Log.d(TAG, "Release of recording session requested."); runOnHandler( mMainThreadHandler, - new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Releasing of recording session."); - mTuned = false; - mClient.release(); - mClient = null; - for (TvViewSession session : mTvViewSessions) { - if (DEBUG) { - Log.d( - TAG, - "Finding TvView sessions for retune: {tuned=" - + session.mTuned - + ", inputId=" - + session.mInputId - + ", session=" - + session - + "}"); - } - if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { - session.retune(); - break; - } + () -> { + if (DEBUG) Log.d(TAG, "Releasing of recording session."); + mTuned = false; + mClient.release(); + mClient = null; + for (TvViewSession session : mTvViewSessions) { + if (DEBUG) { + Log.d( + TAG, + "Finding TvView sessions for retune: {tuned=" + + session.mTuned + + ", inputId=" + + session.mInputId + + ", session=" + + session + + "}"); + } + if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { + session.retune(); + break; } } }); @@ -441,42 +456,39 @@ public class InputSessionManager { public void tune(String inputId, Uri channelUri) { runOnHandler( mMainThreadHandler, - new Runnable() { - @Override - public void run() { - int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - if (input == null - || !input.canRecord() - || input.getTunerCount() <= tunedRecordingSessionCount) { - runOnHandler( - mHandler, - new Runnable() { - @Override - public void run() { - mCallback.onConnectionFailed(inputId); - } - }); - return; - } - mTuned = true; - int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); - if (!isTunedForTvView(channelUri) - && tunedTuneSessionCount > 0 - && tunedRecordingSessionCount + tunedTuneSessionCount - >= input.getTunerCount()) { - for (TvViewSession session : mTvViewSessions) { - if (session.mTuned - && Objects.equals(session.mInputId, inputId) - && !isTunedForRecording(session.mChannelUri)) { - session.resetByRecording(); - break; - } + () -> { + int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + if (input == null + || !input.canRecord() + || input.getTunerCount() <= tunedRecordingSessionCount) { + runOnHandler( + mHandler, + new Runnable() { + @Override + public void run() { + mCallback.onConnectionFailed(inputId); + } + }); + return; + } + mTuned = true; + int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); + if (!isTunedForTvView(channelUri) + && tunedTuneSessionCount > 0 + && tunedRecordingSessionCount + tunedTuneSessionCount + >= input.getTunerCount()) { + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned + && Objects.equals(session.mInputId, inputId) + && !isTunedForRecording(session.mChannelUri)) { + session.resetByRecording(); + break; } } - mChannelUri = channelUri; - mClient.tune(inputId, channelUri); } + mChannelUri = channelUri; + mClient.tune(inputId, channelUri); }); } @@ -504,10 +516,10 @@ public class InputSessionManager { } } - private static class DelegateTvInputCallback extends TvInputCallback { - private final TvInputCallback mDelegate; + private static class DelegateTvInputCallback extends TvInputCallbackCompat { + private final TvInputCallbackCompat mDelegate; - DelegateTvInputCallback(TvInputCallback delegate) { + DelegateTvInputCallback(TvInputCallbackCompat delegate) { mDelegate = delegate; } @@ -565,6 +577,11 @@ public class InputSessionManager { public void onTimeShiftStatusChanged(String inputId, int status) { mDelegate.onTimeShiftStatusChanged(inputId, status); } + + @Override + public void onSignalStrength(String inputId, int value) { + mDelegate.onSignalStrength(inputId, value); + } } /** 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 94a86cce..b4cf71db 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -22,7 +22,6 @@ import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; @@ -49,6 +48,7 @@ import android.provider.Settings; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -65,16 +65,22 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.Toast; +import com.android.tv.MainActivity.MySingletons; import com.android.tv.analytics.SendChannelStatusRunnable; import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; +import com.android.tv.audio.AudioManagerHelper; +import com.android.tv.audiotvservice.AudioOnlyTvServiceUtil; import com.android.tv.common.BuildConfig; +import com.android.tv.common.CommonConstants; import com.android.tv.common.CommonPreferences; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.WeakHandler; +import com.android.tv.common.compat.TvInputInfoCompat; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.memory.MemoryManageable; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.util.CommonUtils; import com.android.tv.common.util.ContentUriUtils; @@ -99,17 +105,19 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.recorder.ConflictChecker; import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.features.TvFeatures; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.perf.EventNames; -import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.TimerEvent; +import com.android.tv.perf.PerformanceMonitorManagerFactory; +import com.android.tv.receiver.AudioCapabilitiesReceiver; import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.recommendation.NotificationService; import com.android.tv.search.ProgramGuideSearchFragment; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; import com.android.tv.ui.ChannelBannerView; +import com.android.tv.ui.DetailsActivity; import com.android.tv.ui.InputBannerView; import com.android.tv.ui.KeypadChannelSwitchView; import com.android.tv.ui.SelectInputView; @@ -128,6 +136,7 @@ import com.android.tv.ui.sidepanel.SettingsFragment; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment; import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.AsyncDbTask.DbExecutor; import com.android.tv.util.CaptionSettings; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.RecurringRunner; @@ -140,6 +149,10 @@ import com.android.tv.util.ViewCache; import com.android.tv.util.account.AccountHelper; import com.android.tv.util.images.ImageCache; +import com.google.common.base.Optional; +import dagger.android.AndroidInjection; +import dagger.android.ContributesAndroidInjector; +import com.android.tv.common.flags.BackendKnobsFlags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; @@ -150,11 +163,21 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Provider; /** The main activity for the Live TV app. */ -public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener { +public class MainActivity extends Activity + implements OnActionClickListener, + OnPinCheckedListener, + ChannelChanger, + HasSingletons<MySingletons> { private static final String TAG = "MainActivity"; private static final boolean DEBUG = false; + private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; + + /** Singletons needed for this class. */ + public interface MySingletons extends ChannelBannerView.MySingletons {} @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -175,6 +198,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private static final float FRAME_RATE_FOR_FILM = 23.976f; private static final float FRAME_RATE_EPSILON = 0.1f; +// AOSP_Comment_Out private static final String PLUTO_TV_PACKAGE_NAME = "tv.pluto.android"; + private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; @@ -232,10 +257,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private static final int UNDEFINED_TRACK_INDEX = -1; private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3); + { + PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit(); + } + + private final MySingletonsImpl mMySingletons = new MySingletonsImpl(); + @Inject @DbExecutor Executor mDbExecutor; + private AccessibilityManager mAccessibilityManager; - private ChannelDataManager mChannelDataManager; - private ProgramDataManager mProgramDataManager; - private TvInputManagerHelper mTvInputManagerHelper; + @Inject ChannelDataManager mChannelDataManager; + @Inject ProgramDataManager mProgramDataManager; + @Inject TvInputManagerHelper mTvInputManagerHelper; private ChannelTuner mChannelTuner; private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this); private TvViewUiManager mTvViewUiManager; @@ -245,10 +277,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private final DurationTimer mTuneDurationTimer = new DurationTimer(); private DvrManager mDvrManager; private ConflictChecker mDvrConflictChecker; - private SetupUtils mSetupUtils; + @Inject BackendKnobsFlags mBackendKnobs; + @Inject SetupUtils mSetupUtils; + @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager; + @VisibleForTesting protected TunableTvView mTvView; private View mContentView; - private TunableTvView mTvView; private Bundle mTuneParams; @Nullable private Uri mInitChannelUri; @Nullable private String mParentInputIdWhenScreenOff; @@ -274,9 +308,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private boolean mNeedShowBackKeyGuide; private boolean mVisibleBehind; private boolean mShowNewSourcesFragment = true; - private String mTunerInputId; private boolean mOtherActivityLaunched; - private PerformanceMonitor mPerformanceMonitor; private boolean mIsInPIPMode; private boolean mIsFilmModeSet; @@ -304,6 +336,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private RecurringRunner mSendConfigInfoRecurringRunner; private RecurringRunner mChannelStatusRecurringRunner; + private String mLastInputIdFromIntent; + private final Handler mHandler = new MainActivityHandler(this); private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>(); @@ -399,28 +433,27 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP public void onChannelChanged(Channel previousChannel, Channel currentChannel) {} }; - private final Runnable mRestoreMainViewRunnable = - new Runnable() { - @Override - public void run() { - restoreMainTvView(); - } - }; + private final Runnable mRestoreMainViewRunnable = this::restoreMainTvView; private ProgramGuideSearchFragment mSearchFragment; private final TvInputCallback mTvInputCallback = new TvInputCallback() { @Override public void onInputAdded(String inputId) { - if (TvFeatures.TUNER.isEnabled(MainActivity.this) - && mTunerInputId.equals(inputId) + if (mOptionalBuiltInTunerManager.isPresent() && CommonPreferences.shouldShowSetupActivity(MainActivity.this)) { - Intent intent = - TvSingletons.getSingletons(MainActivity.this) - .getTunerSetupIntent(MainActivity.this); - startActivity(intent); - CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false); - mSetupUtils.markAsKnownInput(mTunerInputId); + BuiltInTunerManager builtInTunerManager = + mOptionalBuiltInTunerManager.get(); + String tunerInputId = builtInTunerManager.getEmbeddedTunerInputId(); + if (tunerInputId.equals(inputId)) { + Intent intent = + builtInTunerManager + .getTunerInputController() + .createSetupIntent(MainActivity.this); + startActivity(intent); + CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false); + mSetupUtils.markAsKnownInput(tunerInputId); + } } } }; @@ -435,12 +468,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } @Override + public MySingletons singletons() { + return mMySingletons; + } + + @Override protected void onCreate(Bundle savedInstanceState) { + AndroidInjection.inject(this); mAccessibilityManager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); TvSingletons tvSingletons = TvSingletons.getSingletons(this); - mPerformanceMonitor = tvSingletons.getPerformanceMonitor(); - TimerEvent timer = mPerformanceMonitor.startTimer(); DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER); if (!startUpDebugTimer.isStarted() || startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) { @@ -454,16 +491,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } Starter.start(this); super.onCreate(savedInstanceState); - if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) { + if (!mTvInputManagerHelper.hasTvInputManager()) { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); finishAndRemoveTask(); return; } - mPerformanceMonitor = tvSingletons.getPerformanceMonitor(); - mSetupUtils = tvSingletons.getSetupUtils(); - TvApplication tvApplication = (TvApplication) getApplication(); - mChannelDataManager = tvApplication.getChannelDataManager(); + TvSingletons tvApplication = (TvSingletons) getApplication(); // In API 23, TvContract.isChannelUriForPassthroughInput is hidden. boolean isPassthroughInput = TvContract.isChannelUriForPassthroughInput(getIntent().getData()); @@ -480,17 +514,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; } setContentView(R.layout.activity_tv); - mProgramDataManager = tvApplication.getProgramDataManager(); - mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view); mTvView.initialize(mProgramDataManager, mTvInputManagerHelper); mTvView.setOnUnhandledInputEventListener( new OnUnhandledInputEventListener() { @Override public boolean onUnhandledInputEvent(InputEvent event) { - if (DEBUG) { - Log.d(TAG, "onUnhandledInputEvent " + event); - } if (isKeyEventBlocked()) { return true; } @@ -511,7 +540,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return false; } }); - mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null)); + mTvView.setBlockedInfoOnClickListener(v -> showPinDialogFragment()); long channelId = Utils.getLastWatchedChannelId(this); String inputId = Utils.getLastWatchedTunerInputId(this); if (!isPassthroughInput @@ -525,10 +554,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); } mTracker = tvApplication.getTracker(); - if (TvFeatures.TUNER.isEnabled(this)) { + if (mOptionalBuiltInTunerManager.isPresent()) { mTvInputManagerHelper.addCallback(mTvInputCallback); } - mTunerInputId = tvSingletons.getEmbeddedTunerInputId(); mProgramDataManager.addOnCurrentProgramUpdatedListener( Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.setPrefetchEnabled(true); @@ -657,6 +685,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager); mAudioManagerHelper = new AudioManagerHelper(this, mTvView); + 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); @@ -687,7 +717,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } initForTest(); Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end"); - mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONCREATE); } private void startOnboardingActivity() { @@ -778,7 +807,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP @Override protected void onStart() { - TimerEvent timer = mPerformanceMonitor.startTimer(); if (DEBUG) { Log.d(TAG, "onStart()"); } @@ -796,15 +824,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); startService(notificationIntent); } - TvSingletons singletons = TvSingletons.getSingletons(this); - singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this); - singletons.getEpgFetcher().fetchImmediatelyIfNeeded(); - mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART); + if (mOptionalBuiltInTunerManager.isPresent()) { + mOptionalBuiltInTunerManager + .get() + .getTunerInputController() + .executeNetworkTunerDiscoveryAsyncTask(this); + } + TvSingletons.getSingletons(this).getEpgFetcher().fetchImmediatelyIfNeeded(); } @Override protected void onResume() { - TimerEvent timer = mPerformanceMonitor.startTimer(); Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start"); if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); @@ -836,13 +866,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE) && !failedScheduledRecordingInfoSet.isEmpty()) { runAfterAttachedToWindow( - new Runnable() { - @Override - public void run() { + () -> DvrUiHelper.showDvrInsufficientSpaceErrorDialog( - MainActivity.this, failedScheduledRecordingInfoSet); - } - }); + MainActivity.this, failedScheduledRecordingInfoSet)); } if (mChannelTuner.areAllChannelsLoaded()) { @@ -861,32 +887,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // This will delay the start of the animation until after the Live Channel app is // shown. Without this the animation is completed before it is actually visible on // the screen. - mHandler.post( - new Runnable() { - @Override - public void run() { - mOverlayManager.showProgramGuide(); - } - }); + mHandler.post(() -> mOverlayManager.showProgramGuide()); } else if (mShowSelectInputView) { mShowSelectInputView = false; // mShowSelectInputView is true when the activity is started/resumed because the // TV_INPUT button was pressed in a different app. This will delay the start of // the animation until after the Live Channel app is shown. Without this the // animation is completed before it is actually visible on the screen. - mHandler.post( - new Runnable() { - @Override - public void run() { - mOverlayManager.showSelectInputView(); - } - }); + mHandler.post(() -> mOverlayManager.showSelectInputView()); } if (mDvrConflictChecker != null) { mDvrConflictChecker.start(); } + if (CommonFeatures.ENABLE_TV_SERVICE.isEnabled(this) && isAudioOnlyInput()) { + // TODO(b/110969180): figure out when to call AudioOnlyTvServiceUtil.stopAudioOnlyInput + AudioOnlyTvServiceUtil.startAudioOnlyInput(this, mLastInputIdFromIntent); + } Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end"); - mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONRESUME); } @Override @@ -913,7 +930,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } else { mTracker.sendScreenView(SCREEN_BEHIND_NAME); } - TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this); super.onPause(); } @@ -1068,6 +1084,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP markCurrentChannelDuringScreenOff(); } } + if (mChannelTuner.isCurrentChannelPassthrough()) { + mInitChannelUri = mChannelTuner.getCurrentChannelUri(); + } mActivityStarted = false; stopAll(false); unregisterReceiver(mBroadcastReceiver); @@ -1299,19 +1318,15 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) { final Channel channel = returnChannel; Runnable tuneAction = - new Runnable() { - @Override - public void run() { - tuneToChannel(channel); - if (mChannelBeforeShrunkenTvView == null - || !mChannelBeforeShrunkenTvView.equals(channel)) { - Utils.setLastWatchedChannel(MainActivity.this, channel); - } - mIsCompletingShrunkenTvView = false; - mIsCurrentChannelUnblockedByUser = - mWasChannelUnblockedBeforeShrunkenByUser; - mTvView.setBlockScreenType(getDesiredBlockScreenType()); + () -> { + tuneToChannel(channel); + if (mChannelBeforeShrunkenTvView == null + || !mChannelBeforeShrunkenTvView.equals(channel)) { + Utils.setLastWatchedChannel(MainActivity.this, channel); } + mIsCompletingShrunkenTvView = false; + mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; + mTvView.setBlockScreenType(getDesiredBlockScreenType()); }; mTvViewUiManager.fadeOutTvView(tuneAction); // Will automatically fade-in when video becomes available. @@ -1423,17 +1438,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP /** Notifies the key input focus is changed to the TV view. */ public void updateKeyInputFocus() { - mHandler.post( - new Runnable() { - @Override - public void run() { - mTvView.setBlockScreenType(getDesiredBlockScreenType()); - } - }); + mHandler.post(() -> mTvView.setBlockScreenType(getDesiredBlockScreenType())); } // It should be called before onResume. private boolean handleIntent(Intent intent) { + mLastInputIdFromIntent = getInputId(intent); // Reset the closed caption settings when the activity is 1)created or 2) restarted. // And do not reset while TvView is playing. if (!mTvView.isPlaying()) { @@ -1455,13 +1465,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) { - runAfterAttachedToWindow( - new Runnable() { - @Override - public void run() { - mOverlayManager.showSetupFragment(); - } - }); + runAfterAttachedToWindow(() -> mOverlayManager.showSetupFragment()); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); if (Utils.isProgramsUri(uri)) { @@ -1497,8 +1501,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri); if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) { new AsyncQueryProgramTask( - TvSingletons.getSingletons(this).getDbExecutor(), - getContentResolver(), + mDbExecutor, programUriFromIntent, Program.PROJECTION, null, @@ -1565,14 +1568,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP public AsyncQueryProgramTask( Executor executor, - ContentResolver contentResolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy, long channelId) { - super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); + super(executor, MainActivity.this, uri, projection, selection, selectionArgs, orderBy); mChannelIdFromIntent = channelId; } @@ -1593,26 +1595,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent); if (channel != null) { - ScheduledRecording scheduledRecording = - TvSingletons.getSingletons(MainActivity.this) - .getDvrDataManager() - .getScheduledRecordingForProgramId(program.getId()); - DvrUiHelper.checkStorageStatusAndShowErrorMessage( - MainActivity.this, - channel.getInputId(), - new Runnable() { - @Override - public void run() { - if (CommonFeatures.DVR.isEnabled(MainActivity.this) - && scheduledRecording == null - && mDvrManager.isProgramRecordable(program)) { - DvrUiHelper.requestRecordingFutureProgram( - MainActivity.this, program, false); - } else { - DvrUiHelper.showProgramInfoDialog(MainActivity.this, program); - } - } - }); + Intent intent = new Intent(MainActivity.this, DetailsActivity.class); + intent.putExtra(DetailsActivity.CHANNEL_ID, mChannelIdFromIntent); + intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, DetailsActivity.PROGRAM_VIEW); + intent.putExtra(DetailsActivity.PROGRAM, program); + intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId()); + startActivity(intent); } } } @@ -1671,6 +1659,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; } mTunePending = false; + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(this)) { + mTvView.resetChannelSignalStrength(); + mOverlayManager.updateChannelBannerAndShowIfNeeded( + TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH); + } final Channel channel = mChannelTuner.getCurrentChannel(); SoftPreconditions.checkState(channel != null); if (channel == null) { @@ -1717,18 +1710,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP && mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) { // Show new channel sources fragment. runAfterAttachedToWindow( - new Runnable() { - @Override - public void run() { + () -> mOverlayManager.runAfterOverlaysAreClosed( new Runnable() { @Override public void run() { mOverlayManager.showNewSourcesFragment(); } - }); - } - }); + })); } mSetupUtils.onTuned(); if (mTuneParams != null) { @@ -1799,12 +1788,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // should be closed when the activity is paused. private void runAfterAttachedToWindow(final Runnable runnable) { final Runnable runOnlyIfActivityIsResumed = - new Runnable() { - @Override - public void run() { - if (mActivityResumed) { - runnable.run(); - } + () -> { + if (mActivityResumed) { + runnable.run(); } }; if (mContentView.isAttachedToWindow()) { @@ -1918,25 +1904,36 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP window.setAttributes(layoutParams); } - private void applyMultiAudio() { + @VisibleForTesting + protected void applyMultiAudio(String trackId) { List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO); if (tracks == null) { mTvOptionsManager.onMultiAudioChanged(null); return; } - String id = TvSettings.getMultiAudioId(this); - String language = TvSettings.getMultiAudioLanguage(this); - int channelCount = TvSettings.getMultiAudioChannelCount(this); - TvTrackInfo bestTrack = - TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount); + TvTrackInfo bestTrack = null; + if (trackId != null) { + for (TvTrackInfo track : tracks) { + if (trackId.equals(track.getId())) { + bestTrack = track; + break; + } + } + } + if (bestTrack == null) { + String id = TvSettings.getMultiAudioId(this); + String language = TvSettings.getMultiAudioLanguage(this); + int channelCount = TvSettings.getMultiAudioChannelCount(this); + bestTrack = TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount); + } if (bestTrack != null) { String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO); if (!bestTrack.getId().equals(selectedTrack)) { selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX); } else { mTvOptionsManager.onMultiAudioChanged( - Utils.getMultiAudioString(this, bestTrack, false)); + TvTrackInfoUtils.getMultiAudioString(this, bestTrack, false)); } return; } @@ -2056,8 +2053,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (mMediaSessionWrapper != null) { mMediaSessionWrapper.release(); } - if (mAudioManagerHelper != null) { - mAudioManagerHelper.release(); + if (mAudioCapabilitiesReceiver != null) { + mAudioCapabilitiesReceiver.unregister(); } mHandler.removeCallbacksAndMessages(null); application.getMainActivityWrapper().onMainActivityDestroyed(this); @@ -2071,7 +2068,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if (mTvInputManagerHelper != null) { mTvInputManagerHelper.clearTvInputLabels(); - if (TvFeatures.TUNER.isEnabled(this)) { + if (mOptionalBuiltInTunerManager.isPresent()) { mTvInputManagerHelper.removeCallback(mTvInputCallback); } } @@ -2100,51 +2097,59 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (!mChannelTuner.areAllChannelsLoaded()) { return false; } - if (handleUpDownKeys(keyCode, event)) { - return true; - } - return super.onKeyDown(keyCode, event); - } - - private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) { if (!mChannelTuner.isCurrentChannelPassthrough()) { switch (keyCode) { case KeyEvent.KEYCODE_CHANNEL_UP: case KeyEvent.KEYCODE_DPAD_UP: - if ((event == null || event.getRepeatCount() == 0) + if (event.getRepeatCount() == 0 && mChannelTuner.getBrowsableChannelCount() > 0) { - // message sending should be done before moving channel, because we use the - // existence of message to decide if users are switching channel. - if (event != null) { - mHandler.sendMessageDelayed( - mHandler.obtainMessage( - MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()), - CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); - } - moveToAdjacentChannel(true, false); - mTracker.sendChannelUp(); + + channelUpPressed(); } return true; case KeyEvent.KEYCODE_CHANNEL_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN: - if ((event == null || event.getRepeatCount() == 0) + if (event.getRepeatCount() == 0 && mChannelTuner.getBrowsableChannelCount() > 0) { - // message sending should be done before moving channel, because we use the - // existence of message to decide if users are switching channel. - if (event != null) { - mHandler.sendMessageDelayed( - mHandler.obtainMessage( - MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()), - CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); - } - moveToAdjacentChannel(false, false); - mTracker.sendChannelDown(); + channelDownPressed(); } return true; default: // fall out } } - return false; + return super.onKeyDown(keyCode, event); + } + + @Override + public void channelDown() { + channelDownPressed(); + finishChannelChangeIfNeeded(); + } + + private void channelDownPressed() { + // message sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()), + CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + moveToAdjacentChannel(false, false); + mTracker.sendChannelDown(); + } + + @Override + public void channelUp() { + channelUpPressed(); + finishChannelChangeIfNeeded(); + } + + private void channelUpPressed() { + // message sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()), + CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + moveToAdjacentChannel(true, false); + mTracker.sendChannelUp(); } @Override @@ -2228,24 +2233,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP this, mChannelTuner.getCurrentChannel()); return true; } - if (!PermissionUtils.hasModifyParentalControls(this)) { - return true; - } - PinDialogFragment dialog = null; - if (mTvView.isScreenBlocked()) { - dialog = - PinDialogFragment.create( - PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL); - } else if (mTvView.isContentBlocked()) { - dialog = - PinDialogFragment.create( - PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - mTvView.getBlockedContentRating().flattenToString()); - } - if (dialog != null) { - mOverlayManager.showDialogFragment( - PinDialogFragment.DIALOG_TAG, dialog, false); - } + showPinDialogFragment(); return true; case KeyEvent.KEYCODE_WINDOW: enterPictureInPictureMode(); @@ -2315,16 +2303,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP DvrUiHelper.checkStorageStatusAndShowErrorMessage( this, currentChannel.getInputId(), - new Runnable() { - @Override - public void run() { + () -> DvrUiHelper.requestRecordingCurrentProgram( MainActivity.this, currentChannel, program, - false); - } - }); + false)); } } else { DvrUiHelper.showStopRecordingDialog( @@ -2391,6 +2375,24 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return super.onKeyUp(keyCode, event); } + private void showPinDialogFragment() { + if (!PermissionUtils.hasModifyParentalControls(this)) { + return; + } + PinDialogFragment dialog = null; + if (mTvView.isScreenBlocked()) { + dialog = PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL); + } else if (mTvView.isContentBlocked()) { + dialog = + PinDialogFragment.create( + PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + mTvView.getBlockedContentRating().flattenToString()); + } + if (dialog != null) { + mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, false); + } + } + @Override public boolean onKeyLongPress(int keyCode, KeyEvent event) { if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event); @@ -2423,13 +2425,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mIsInPIPMode = true; if (mOverlayManager.isOverlayOpened()) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); - mHandler.post( - new Runnable() { - @Override - public void run() { - MainActivity.super.enterPictureInPictureMode(); - } - }); + mHandler.post(MainActivity.super::enterPictureInPictureMode); } else { MainActivity.super.enterPictureInPictureMode(); } @@ -2586,7 +2582,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mTvView.selectTrack(type, track == null ? null : track.getId()); if (type == TvTrackInfo.TYPE_AUDIO) { mTvOptionsManager.onMultiAudioChanged( - track == null ? null : Utils.getMultiAudioString(this, track, false)); + track == null + ? null + : TvTrackInfoUtils.getMultiAudioString(this, track, false)); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex); } @@ -2594,7 +2592,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP public void selectAudioTrack(String trackId) { saveMultiAudioSetting(trackId); - applyMultiAudio(); + applyMultiAudio(trackId); } private void saveMultiAudioSetting(String trackId) { @@ -2657,6 +2655,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: return; + case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED: + Toast.makeText( + this, + R.string.msg_channel_unavailable_not_connected, + Toast.LENGTH_SHORT) + .show(); + break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT) @@ -2725,14 +2730,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mLazyInitialized = true; // Running initialization. mHandler.postDelayed( - new Runnable() { - @Override - public void run() { - if (mActivityStarted) { - initAnimations(); - initSideFragments(); - initMenuItemViews(); - } + () -> { + if (mActivityStarted) { + initAnimations(); + initSideFragments(); + initMenuItemViews(); } }, LAZY_INITIALIZATION_DELAY); @@ -2751,6 +2753,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mOverlayManager.getMenu().preloadItemViews(); } + private boolean isAudioOnlyInput() { + if (mLastInputIdFromIntent == null) { + return false; + } + TvInputInfoCompat inputInfo = + mTvInputManagerHelper.getTvInputInfoCompat(mLastInputIdFromIntent); + return inputInfo != null && inputInfo.isAudioOnly(); + } + + @Nullable + private String getInputId(Intent intent) { + Uri uri = intent.getData(); + return TvContract.isChannelUriForPassthroughInput(uri) + ? uri.getPathSegments().get(1) + : null; + } + @Override public void onTrimMemory(int level) { super.onTrimMemory(level); @@ -2793,15 +2812,22 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } } - private class MyOnTuneListener implements OnTuneListener { + /** {@link OnTuneListener} implementation */ + @VisibleForTesting + protected class MyOnTuneListener implements OnTuneListener { boolean mUnlockAllowedRatingBeforeShrunken = true; boolean mWasUnderShrunkenTvView; Channel mChannel; - private void onTune(Channel channel, boolean wasUnderShrukenTvView) { + private void onTune(Channel channel, boolean wasUnderShrunkenTvView) { Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune"); mChannel = channel; - mWasUnderShrunkenTvView = wasUnderShrukenTvView; + mWasUnderShrunkenTvView = wasUnderShrunkenTvView; + + if (mBackendKnobs.enablePartialProgramFetch()) { + // Fetch complete projection of tuned channel. + mProgramDataManager.prefetchChannel(channel.getId()); + } } @Override @@ -2824,7 +2850,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } @Override - public void onStreamInfoChanged(StreamInfo info) { + public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) { if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) { mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset()); } @@ -2834,7 +2860,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } applyDisplayRefreshRate(info.getVideoFrameRate()); mTvViewUiManager.updateTvAspectRatio(); - applyMultiAudio(); + applyMultiAudio( + allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO)); applyClosedCaption(); mOverlayManager.getMenu().onStreamInfoChanged(); if (mTvView.isVideoAvailable()) { @@ -2861,6 +2888,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP + channel); return; } + /* Begin_AOSP_Comment_Out + if (PLUTO_TV_PACKAGE_NAME.equals(currentChannel.getPackageName())) { + // Do nothing for the Pluto TV input because it misuses this API. b/22720711. + return; + } + End_AOSP_Comment_Out */ if (isChannelChangeKeyDownReceived()) { // Ignore this message if the user is changing the channel. return; @@ -2883,7 +2916,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // before. if (mWasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken - && mChannelBeforeShrunkenTvView.equals(mChannel) + && Objects.equals(mChannelBeforeShrunkenTvView, mChannel) && rating.equals(mAllowedRatingBeforeShrunken)) { mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); mTvView.unblockContent(rating); @@ -2901,5 +2934,53 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mOverlayManager.setBlockingContentRating(null); mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram()); } + + @Override + public void onChannelSignalStrength() { + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(getApplicationContext())) { + mOverlayManager.updateChannelBannerAndShowIfNeeded( + TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH); + } + } + } + + private class MySingletonsImpl implements MySingletons { + + @Override + public Provider<Channel> getCurrentChannelProvider() { + return MainActivity.this::getCurrentChannel; + } + + @Override + public Provider<Program> getCurrentProgramProvider() { + return MainActivity.this::getCurrentProgram; + } + + @Override + public Provider<TvOverlayManager> getOverlayManagerProvider() { + return MainActivity.this::getOverlayManager; + } + + @Override + public TvInputManagerHelper getTvInputManagerHelperSingleton() { + return getTvInputManagerHelper(); + } + + @Override + public Provider<Long> getCurrentPlayingPositionProvider() { + return MainActivity.this::getCurrentPlayingPosition; + } + + @Override + public DvrManager getDvrManagerSingleton() { + return TvSingletons.getSingletons(getApplicationContext()).getDvrManager(); + } + } + + /** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract MainActivity contributesMainActivityActivityInjector(); } } diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java index 43cd74dd..a647a06f 100644 --- a/src/com/android/tv/MediaSessionWrapper.java +++ b/src/com/android/tv/MediaSessionWrapper.java @@ -16,12 +16,14 @@ package com.android.tv; +import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadata; +import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.media.tv.TvContract; @@ -31,6 +33,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import android.util.Log; import com.android.tv.data.Program; import com.android.tv.data.api.Channel; import com.android.tv.util.Utils; @@ -41,9 +44,12 @@ import com.android.tv.util.images.ImageLoader; * {@link MainActivity}. */ class MediaSessionWrapper { + private static final String TAG = "MediaSessionWrapper"; + private static final boolean DEBUG = false; private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession"; - private static final PlaybackState MEDIA_SESSION_STATE_PLAYING = + @VisibleForTesting + static final PlaybackState MEDIA_SESSION_STATE_PLAYING = new PlaybackState.Builder() .setState( PlaybackState.STATE_PLAYING, @@ -51,7 +57,8 @@ class MediaSessionWrapper { 1.0f) .build(); - private static final PlaybackState MEDIA_SESSION_STATE_STOPPED = + @VisibleForTesting + static final PlaybackState MEDIA_SESSION_STATE_STOPPED = new PlaybackState.Builder() .setState( PlaybackState.STATE_STOPPED, @@ -61,6 +68,20 @@ class MediaSessionWrapper { private final Context mContext; private final MediaSession mMediaSession; + private final MediaController.Callback mMediaControllerCallback = + new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState state) { + super.onPlaybackStateChanged(state); + if (DEBUG) { + Log.d(TAG, "onPlaybackStateChanged: " + state); + } + if (isMediaSessionStateStop(state)) { + mMediaSession.setActive(false); + } + } + }; + private MediaController mMediaController; private int mNowPlayingCardWidth; private int mNowPlayingCardHeight; @@ -79,6 +100,8 @@ class MediaSessionWrapper { MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setSessionActivity(pendingIntent); + + initMediaController(); mNowPlayingCardWidth = mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); mNowPlayingCardHeight = @@ -97,7 +120,6 @@ class MediaSessionWrapper { mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING); } else if (mMediaSession.isActive()) { mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED); - mMediaSession.setActive(false); } } @@ -150,6 +172,7 @@ class MediaSessionWrapper { * @see MediaSession#release() */ void release() { + unregisterMediaControllerCallback(); mMediaSession.release(); } @@ -223,6 +246,30 @@ class MediaSessionWrapper { return mMediaSession; } + @VisibleForTesting + MediaController.Callback getMediaControllerCallback() { + return mMediaControllerCallback; + } + + @VisibleForTesting + void initMediaController() { + mMediaController = new MediaController(mContext, mMediaSession.getSessionToken()); + ((Activity) mContext).setMediaController(mMediaController); + mMediaController.registerCallback(mMediaControllerCallback); + } + + @VisibleForTesting + void unregisterMediaControllerCallback() { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + + private static boolean isMediaSessionStateStop(PlaybackState state) { + return state != null + && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState() + && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition() + && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed(); + } + private static class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> { private final Channel mChannel; diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index 199ea51d..5185b122 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -28,11 +28,11 @@ import android.support.annotation.MainThread; import android.util.Log; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.actions.InputSetupActionUtils; -import com.android.tv.common.experiments.Experiments; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; import com.android.tv.data.epg.EpgFetcher; import com.android.tv.data.epg.EpgInputWhiteList; +import com.android.tv.features.TvFeatures; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -66,12 +66,10 @@ public class SetupPassthroughActivity extends Activity { Intent intent = getIntent(); String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID); mTvInputInfo = inputManager.getTvInputInfo(inputId); - mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig()); + mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getCloudEpgFlags()); mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent); boolean needToFetchEpg = - mTvInputInfo != null - && Utils.isInternalTvInput(this, mTvInputInfo.getId()) - && Experiments.CLOUD_EPG.get(); + mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId()); if (needToFetchEpg) { // In case when the activity is restored, this flag should be restored as well. mEpgFetcherDuringScan = true; @@ -144,23 +142,30 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } + if (mTvInputInfo == null) { + Log.w( + TAG, + "There is no input with ID " + + getIntent().getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID) + + "."); + setResult(resultCode, data); + finish(); + return; + } TvSingletons.getSingletons(this) .getSetupUtils() .onTvInputSetupFinished( mTvInputInfo.getId(), - new Runnable() { - @Override - public void run() { - if (mActivityAfterCompletion != null) { - try { - startActivity(mActivityAfterCompletion); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Activity launch failed", e); - } + () -> { + if (mActivityAfterCompletion != null) { + try { + startActivity(mActivityAfterCompletion); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity launch failed", e); } - setResult(resultCode, data); - finish(); } + setResult(resultCode, data); + finish(); }); } @@ -178,15 +183,12 @@ public class SetupPassthroughActivity extends Activity { private final ChannelDataManager mChannelDataManager; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Runnable mScanTimeoutRunnable = - new Runnable() { - @Override - public void run() { - Log.w( - TAG, - "No channels has been added for a while." - + " The scan might have finished unexpectedly."); - onScanTimedOut(); - } + () -> { + Log.w( + TAG, + "No channels has been added for a while." + + " The scan might have finished unexpectedly."); + onScanTimedOut(); }; private final Listener mChannelDataManagerListener = new Listener() { diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index bb3574d7..779e8df6 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -17,7 +17,6 @@ package com.android.tv; import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; import android.os.Handler; import android.os.Message; @@ -35,7 +34,7 @@ import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; import com.android.tv.data.api.Channel; import com.android.tv.ui.TunableTvView; -import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener; +import com.android.tv.ui.api.TunableTvViewPlayingApi.TimeShiftListener; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; @@ -87,16 +86,15 @@ public class TimeShiftManager { @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = { - TIME_SHIFT_ACTION_ID_PLAY, - TIME_SHIFT_ACTION_ID_PAUSE, - TIME_SHIFT_ACTION_ID_REWIND, - TIME_SHIFT_ACTION_ID_FAST_FORWARD, - TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, - TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT - } - ) + flag = true, + value = { + TIME_SHIFT_ACTION_ID_PLAY, + TIME_SHIFT_ACTION_ID_PAUSE, + TIME_SHIFT_ACTION_ID_REWIND, + TIME_SHIFT_ACTION_ID_FAST_FORWARD, + TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, + TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT + }) public @interface TimeShiftActionId {} public static final int TIME_SHIFT_ACTION_ID_PLAY = 1; @@ -715,7 +713,7 @@ public class TimeShiftManager { : mRecordEndTimeMs; long currentPositionMs = Math.max( - Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs), + Math.min(mTvView.timeShiftGetCurrentPositionMs(), currentTimeMs), mRecordStartTimeMs); boolean isCurrentTime = currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD; @@ -723,7 +721,7 @@ public class TimeShiftManager { if (isCurrentTime && isForwarding()) { // It's playing forward and the current playing position reached // the current system time. i.e. The live stream is played. - // Therefore no need to call TvView.timeshiftGetCurrentPositionMs + // Therefore no need to call TvView.timeShiftGetCurrentPositionMs // any more. newCurrentPositionMs = currentTimeMs; mIsPlayOffsetChanged = false; @@ -753,14 +751,14 @@ public class TimeShiftManager { mDisplayedPlaySpeed = PLAY_SPEED_1X; mPlaybackSpeed = 1; mPlayDirection = PLAY_DIRECTION_FORWARD; - mTvView.timeshiftPlay(); + mTvView.timeShiftPlay(); setPlayStatus(PLAY_STATUS_PLAYING); } void pause() { mDisplayedPlaySpeed = PLAY_SPEED_1X; mPlaybackSpeed = 1; - mTvView.timeshiftPause(); + mTvView.timeShiftPause(); setPlayStatus(PLAY_STATUS_PAUSED); mIsPlayOffsetChanged = true; } @@ -783,7 +781,7 @@ public class TimeShiftManager { } mPlayDirection = PLAY_DIRECTION_BACKWARD; mPlaybackSpeed = getPlaybackSpeed(); - mTvView.timeshiftRewind(mPlaybackSpeed); + mTvView.timeShiftRewind(mPlaybackSpeed); setPlayStatus(PLAY_STATUS_PLAYING); mIsPlayOffsetChanged = true; } @@ -796,14 +794,14 @@ public class TimeShiftManager { } mPlayDirection = PLAY_DIRECTION_FORWARD; mPlaybackSpeed = getPlaybackSpeed(); - mTvView.timeshiftFastForward(mPlaybackSpeed); + mTvView.timeShiftFastForward(mPlaybackSpeed); setPlayStatus(PLAY_STATUS_PLAYING); mIsPlayOffsetChanged = true; } /** Moves to the specified time. */ void seekTo(long timeMs) { - mTvView.timeshiftSeekTo( + mTvView.timeShiftSeekTo( Math.min( mRecordEndTimeMs == CURRENT_TIME ? System.currentTimeMillis() @@ -821,9 +819,9 @@ public class TimeShiftManager { if (playbackSpeed != mPlaybackSpeed) { mPlaybackSpeed = playbackSpeed; if (mPlayDirection == PLAY_DIRECTION_FORWARD) { - mTvView.timeshiftFastForward(mPlaybackSpeed); + mTvView.timeShiftFastForward(mPlaybackSpeed); } else { - mTvView.timeshiftRewind(mPlaybackSpeed); + mTvView.timeShiftRewind(mPlaybackSpeed); } } } @@ -977,8 +975,7 @@ public class TimeShiftManager { } } if (mChannel != null) { - mProgramLoadTask = - new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), next); + mProgramLoadTask = new LoadProgramsForCurrentChannelTask(next); mProgramLoadTask.executeOnDbThread(); } } @@ -1225,10 +1222,10 @@ public class TimeShiftManager { private class LoadProgramsForCurrentChannelTask extends AsyncDbTask.LoadProgramsForChannelTask { - LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) { + LoadProgramsForCurrentChannelTask(Range<Long> period) { super( TvSingletons.getSingletons(mContext).getDbExecutor(), - contentResolver, + mContext, mChannel.getId(), period); } @@ -1309,13 +1306,7 @@ public class TimeShiftManager { mProgramLoadTask = null; } // Need to post to handler, because the task is still running. - mHandler.post( - new Runnable() { - @Override - public void run() { - startTaskIfNeeded(); - } - }); + mHandler.post(ProgramManager.this::startTaskIfNeeded); } boolean overlaps(Queue<Range<Long>> programLoadQueue) { diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 826317b9..5f25a24b 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -34,8 +34,8 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; +import android.widget.Toast; import com.android.tv.common.BaseApplication; -import com.android.tv.common.concurrent.NamedThreadFactory; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; @@ -55,17 +55,22 @@ import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import com.android.tv.features.TvFeatures; +import com.android.tv.perf.PerformanceMonitorManager; +import com.android.tv.perf.PerformanceMonitorManagerFactory; import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.recommendation.RecordedProgramPreviewUpdater; -import com.android.tv.tuner.TunerInputController; -import com.android.tv.tuner.util.TunerInputInfoUtils; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; +import com.android.tv.tunerinputcontroller.TunerInputController; +import com.android.tv.util.AsyncDbTask.DbExecutor; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import com.google.common.base.Optional; +import dagger.Lazy; import java.util.List; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import javax.inject.Inject; /** * Live TV application. @@ -73,6 +78,9 @@ import java.util.concurrent.Executors; * <p>This includes all the Google specific hooks. */ public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter { + + protected static final PerformanceMonitorManager PERFORMANCE_MONITOR_MANAGER = + PerformanceMonitorManagerFactory.create(); private static final String TAG = "TvApplication"; private static final boolean DEBUG = false; @@ -89,10 +97,6 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch"; - private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db"); - private static final ExecutorService DB_EXECUTOR = - Executors.newSingleThreadExecutor(THREAD_FACTORY); - private String mVersionName = ""; private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper(); @@ -111,22 +115,28 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet // STOP-SHIP: Remove this variable when Tuner Process is split to another application. // When this variable is null, we don't know in which process TvApplication runs. private Boolean mRunningInMainProcess; - private TvInputManagerHelper mTvInputManagerHelper; + @Inject Lazy<TvInputManagerHelper> mLazyTvInputManagerHelper; private boolean mStarted; private EpgFetcher mEpgFetcher; - private TunerInputController mTunerInputController; + + @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager; + @Inject SetupUtils mSetupUtils; + @Inject @DbExecutor Executor mDbExecutor; @Override public void onCreate() { + if (getSystemService(TvInputManager.class) == null) { + String msg = "Not an Android TV device."; + Toast.makeText(this, msg, Toast.LENGTH_LONG); + Log.wtf(TAG, msg); + throw new IllegalStateException(msg); + } super.onCreate(); SharedPreferencesUtils.initialize( this, - new Runnable() { - @Override - public void run() { - if (mRunningInMainProcess != null && mRunningInMainProcess) { - checkTunerServiceOnFirstLaunch(); - } + () -> { + if (mRunningInMainProcess != null && mRunningInMainProcess) { + checkTunerServiceOnFirstLaunch(); } }); try { @@ -164,13 +174,19 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet new TvInputCallback() { @Override public void onInputAdded(String inputId) { - if (TvFeatures.TUNER.isEnabled(TvApplication.this) - && TextUtils.equals( - inputId, getEmbeddedTunerInputId())) { - TunerInputInfoUtils.updateTunerInputInfo( - TvApplication.this); + if (mOptionalBuiltInTunerManager.isPresent()) { + BuiltInTunerManager builtInTunerManager = + mOptionalBuiltInTunerManager.get(); + if (TextUtils.equals( + inputId, + builtInTunerManager.getEmbeddedTunerInputId())) { + + builtInTunerManager + .getTunerInputController() + .updateTunerInputInfo(TvApplication.this); + } + handleInputCountChanged(); } - handleInputCountChanged(); } @Override @@ -178,10 +194,13 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet handleInputCountChanged(); } }); - if (TvFeatures.TUNER.isEnabled(this)) { + if (mOptionalBuiltInTunerManager.isPresent()) { // If the tuner input service is added before the app is started, we need to // handle it here. - TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this); + mOptionalBuiltInTunerManager + .get() + .getTunerInputController() + .updateTunerInputInfo(TvApplication.this); } if (CommonFeatures.DVR.isEnabled(this)) { mDvrScheduleManager = new DvrScheduleManager(this); @@ -205,8 +224,12 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true); if (isFirstLaunch) { if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!"); - getTunerInputController() - .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED); + if (mOptionalBuiltInTunerManager.isPresent()) { + mOptionalBuiltInTunerManager + .get() + .getTunerInputController() + .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED); + } SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false); editor.apply(); @@ -220,7 +243,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet @Override public synchronized SetupUtils getSetupUtils() { - return SetupUtils.createForTvSingletons(this); + return mSetupUtils; } /** Returns the {@link DvrManager}. */ @@ -282,13 +305,10 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet return mProgramDataManager; } Utils.runInMainThreadAndWait( - new Runnable() { - @Override - public void run() { - if (mProgramDataManager == null) { - mProgramDataManager = new ProgramDataManager(TvApplication.this); - mProgramDataManager.start(); - } + () -> { + if (mProgramDataManager == null) { + mProgramDataManager = new ProgramDataManager(TvApplication.this); + mProgramDataManager.start(); } }); return mProgramDataManager; @@ -340,21 +360,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet /** Returns {@link TvInputManagerHelper}. */ @Override public TvInputManagerHelper getTvInputManagerHelper() { - if (mTvInputManagerHelper == null) { - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); - } - return mTvInputManagerHelper; - } - - @Override - public synchronized TunerInputController getTunerInputController() { - if (mTunerInputController == null) { - mTunerInputController = - new TunerInputController( - ComponentName.unflattenFromString(getEmbeddedTunerInputId())); - } - return mTunerInputController; + return mLazyTvInputManagerHelper.get(); } @Override @@ -480,12 +486,16 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet if (!enable) { List<TvInputInfo> inputs = inputManager.getTvInputList(); boolean skipTunerInputCheck = false; + Optional<String> optionalEmbeddedTunerInputId = + mOptionalBuiltInTunerManager.transform( + BuiltInTunerManager::getEmbeddedTunerInputId); // Enable the TvActivity only if there is at least one tuner type input. if (!skipTunerInputCheck) { for (TvInputInfo input : inputs) { if (calledByTunerServiceChanged && !tunerServiceEnabled - && getEmbeddedTunerInputId().equals(input.getId())) { + && optionalEmbeddedTunerInputId.isPresent() + && optionalEmbeddedTunerInputId.get().equals(input.getId())) { continue; } if (input.getType() == TvInputInfo.TYPE_TUNER) { @@ -507,11 +517,11 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0); Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV."); } - getSetupUtils().onInputListUpdated(inputManager); + mSetupUtils.onInputListUpdated(inputManager); } @Override public Executor getDbExecutor() { - return DB_EXECUTOR; + return mDbExecutor; } } diff --git a/src/com/android/tv/TvSingletons.java b/src/com/android/tv/TvSingletons.java index 0c7f78a3..20edf3d4 100644 --- a/src/com/android/tv/TvSingletons.java +++ b/src/com/android/tv/TvSingletons.java @@ -22,6 +22,7 @@ import com.android.tv.analytics.Tracker; import com.android.tv.common.BaseApplication; import com.android.tv.common.BaseSingletons; import com.android.tv.common.experiments.ExperimentLoader; +import com.android.tv.common.flags.has.HasUiFlags; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.PreviewDataManager; import com.android.tv.data.ProgramDataManager; @@ -33,17 +34,23 @@ import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.tuner.TunerInputController; +import com.android.tv.tunerinputcontroller.HasBuiltInTunerManager; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.account.AccountHelper; +import com.android.tv.common.flags.BackendKnobsFlags; import java.util.concurrent.Executor; import javax.inject.Provider; /** Interface with getters for application scoped singletons. */ -public interface TvSingletons extends BaseSingletons { +public interface TvSingletons extends BaseSingletons, HasBuiltInTunerManager, HasUiFlags { - /** Returns the @{@link TvSingletons} using the application context. */ + /** + * Returns the @{@link TvSingletons} using the application context. + * + * @deprecated use injection instead. + */ + @Deprecated static TvSingletons getSingletons(Context context) { return (TvSingletons) BaseApplication.getSingletons(context); } @@ -52,6 +59,7 @@ public interface TvSingletons extends BaseSingletons { void handleInputCountChanged(); + @Deprecated ChannelDataManager getChannelDataManager(); /** @@ -60,6 +68,8 @@ public interface TvSingletons extends BaseSingletons { */ boolean isChannelDataManagerLoadFinished(); + /** @deprecated use injection instead. */ + @Deprecated ProgramDataManager getProgramDataManager(); /** @@ -92,17 +102,23 @@ public interface TvSingletons extends BaseSingletons { PerformanceMonitor getPerformanceMonitor(); + /** @deprecated use injection instead. */ + @Deprecated TvInputManagerHelper getTvInputManagerHelper(); Provider<EpgReader> providesEpgReader(); EpgFetcher getEpgFetcher(); + /** @deprecated use injection instead. */ + @Deprecated SetupUtils getSetupUtils(); - TunerInputController getTunerInputController(); - ExperimentLoader getExperimentLoader(); + /** @deprecated use injection instead. */ + @Deprecated Executor getDbExecutor(); + + BackendKnobsFlags getBackendKnobs(); } diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java index 4a84434c..306bd855 100644 --- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java +++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java @@ -43,13 +43,7 @@ public class SendChannelStatusRunnable implements Runnable { final SendChannelStatusRunnable sendChannelStatusRunnable = new SendChannelStatusRunnable(channelDataManager, tracker); - Runnable onStopRunnable = - new Runnable() { - @Override - public void run() { - sendChannelStatusRunnable.setDbLoadListener(null); - } - }; + Runnable onStopRunnable = () -> sendChannelStatusRunnable.setDbLoadListener(null); final RecurringRunner recurringRunner = new RecurringRunner( context, @@ -70,14 +64,7 @@ public class SendChannelStatusRunnable implements Runnable { // done // via a post on the main thread new Handler(Looper.getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - sendChannelStatusRunnable.setDbLoadListener( - null); - } - }); + .post(() -> sendChannelStatusRunnable.setDbLoadListener(null)); recurringRunner.start(); } diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java index 461331d5..38e85e48 100644 --- a/src/com/android/tv/app/LiveTvApplication.java +++ b/src/com/android/tv/app/LiveTvApplication.java @@ -16,36 +16,37 @@ package com.android.tv.app; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.media.tv.TvContract; import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.Tracker; -import com.android.tv.common.CommonConstants; -import com.android.tv.common.actions.InputSetupActionUtils; -import com.android.tv.common.config.DefaultConfigManager; -import com.android.tv.common.config.api.RemoteConfig; +import com.android.tv.common.dagger.ApplicationModule; import com.android.tv.common.experiments.ExperimentLoader; -import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags; +import com.android.tv.common.flags.impl.DefaultCloudEpgFlags; +import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags; +import com.android.tv.common.flags.impl.DefaultUiFlags; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.data.epg.EpgReader; import com.android.tv.data.epg.StubEpgReader; +import com.android.tv.modules.TvSingletonsModule; import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.StubPerformanceMonitor; -import com.android.tv.tuner.livetuner.LiveTvTunerTvInputService; -import com.android.tv.tuner.setup.LiveTvTunerSetupActivity; +import com.android.tv.perf.PerformanceMonitorManagerFactory; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; import com.android.tv.util.account.AccountHelper; import com.android.tv.util.account.AccountHelperImpl; +import com.google.common.base.Optional; +import dagger.android.AndroidInjector; import javax.inject.Provider; /** The top level application for Live TV. */ -public class LiveTvApplication extends TvApplication { - protected static final String TV_ACTIVITY_CLASS_NAME = - CommonConstants.BASE_PACKAGE + ".TvActivity"; +public class LiveTvApplication extends TvApplication implements HasSingletons<TvSingletons> { + + static { + PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppClassLoaded(); + } - private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor(); private final Provider<EpgReader> mEpgReaderProvider = new Provider<EpgReader>() { @@ -55,12 +56,30 @@ public class LiveTvApplication extends TvApplication { } }; + private final DefaultBackendKnobsFlags mBackendKnobsFlags = new DefaultBackendKnobsFlags(); + private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags(); + private final DefaultUiFlags mUiFlags = new DefaultUiFlags(); + private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags = + new DefaultConcurrentDvrPlaybackFlags(); private AccountHelper mAccountHelper; private Analytics mAnalytics; private Tracker mTracker; - private String mEmbeddedInputId; - private RemoteConfig mRemoteConfig; private ExperimentLoader mExperimentLoader; + private PerformanceMonitor mPerformanceMonitor; + + @Override + protected AndroidInjector<LiveTvApplication> applicationInjector() { + return DaggerLiveTvApplicationComponent.builder() + .applicationModule(new ApplicationModule(this)) + .tvSingletonsModule(new TvSingletonsModule(this)) + .build(); + } + + @Override + public void onCreate() { + super.onCreate(); + PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppCreate(this); + } /** Returns the {@link AccountHelperImpl}. */ @Override @@ -73,7 +92,10 @@ public class LiveTvApplication extends TvApplication { @Override public synchronized PerformanceMonitor getPerformanceMonitor() { - return performanceMonitor; + if (mPerformanceMonitor == null) { + mPerformanceMonitor = PerformanceMonitorManagerFactory.create().initialize(this); + } + return mPerformanceMonitor; } @Override @@ -87,6 +109,11 @@ public class LiveTvApplication extends TvApplication { return mExperimentLoader; } + @Override + public DefaultBackendKnobsFlags getBackendKnobs() { + return mBackendKnobsFlags; + } + /** Returns the {@link Analytics}. */ @Override public synchronized Analytics getAnalytics() { @@ -106,34 +133,32 @@ public class LiveTvApplication extends TvApplication { } @Override - public Intent getTunerSetupIntent(Context context) { - // Make an intent to launch the setup activity of TV tuner input. - Intent intent = - CommonUtils.createSetupIntent( - new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId); - intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId); - Intent tvActivityIntent = new Intent(); - tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); - intent.putExtra(InputSetupActionUtils.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); - return intent; + public DefaultCloudEpgFlags getCloudEpgFlags() { + return mCloudEpgFlags; } @Override - public synchronized String getEmbeddedTunerInputId() { - if (mEmbeddedInputId == null) { - mEmbeddedInputId = - TvContract.buildInputId( - new ComponentName(this, LiveTvTunerTvInputService.class)); - } - return mEmbeddedInputId; + public DefaultUiFlags getUiFlags() { + return mUiFlags; } @Override - public RemoteConfig getRemoteConfig() { - if (mRemoteConfig == null) { - // No need to synchronize this, it does not hurt to create two and throw one away. - mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig(); - } - return mRemoteConfig; + public Optional<BuiltInTunerManager> getBuiltInTunerManager() { + return Optional.absent(); + } + + @Override + public BuildType getBuildType() { + return BuildType.AOSP; + } + + @Override + public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() { + return mConcurrentDvrPlaybackFlags; + } + + @Override + public TvSingletons singletons() { + return this; } } diff --git a/src/com/android/tv/app/LiveTvApplicationComponent.java b/src/com/android/tv/app/LiveTvApplicationComponent.java new file mode 100644 index 00000000..3d3f0492 --- /dev/null +++ b/src/com/android/tv/app/LiveTvApplicationComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.app; + +import dagger.Component; +import dagger.android.AndroidInjectionModule; +import dagger.android.AndroidInjector; +import javax.inject.Singleton; + +/** Dagger component for {@link LiveTvApplication}. */ +@Singleton +@Component(modules = {AndroidInjectionModule.class, LiveTvModule.class}) +public interface LiveTvApplicationComponent extends AndroidInjector<LiveTvApplication> {} diff --git a/src/com/android/tv/app/LiveTvModule.java b/src/com/android/tv/app/LiveTvModule.java new file mode 100644 index 00000000..a28749bd --- /dev/null +++ b/src/com/android/tv/app/LiveTvModule.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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.app; + +import com.android.tv.common.flags.impl.DefaultFlagsModule; +import com.android.tv.modules.TvApplicationModule; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; +import com.google.common.base.Optional; +import dagger.Module; +import dagger.Provides; + +/** Dagger module for {@link LiveTvApplication}. */ +@Module(includes = {DefaultFlagsModule.class, TvApplicationModule.class}) +class LiveTvModule { + + @Provides + Optional<BuiltInTunerManager> providesBuiltInTunerManager() { + return Optional.absent(); + } +} diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/audio/AudioManagerHelper.java index 942d431d..4acff2d3 100644 --- a/src/com/android/tv/AudioManagerHelper.java +++ b/src/com/android/tv/audio/AudioManagerHelper.java @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.tv; +package com.android.tv.audio; import android.app.Activity; import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.Build; -import com.android.tv.receiver.AudioCapabilitiesReceiver; -import com.android.tv.ui.TunableTvView; -import com.android.tv.ui.TunableTvViewPlayingApi; +import android.support.annotation.Nullable; +import com.android.tv.features.TvFeatures; +import com.android.tv.ui.api.TunableTvViewPlayingApi; -/** A helper class to help {@link MainActivity} to handle audio-related stuffs. */ -class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { +/** A helper class to help {@code Activities} to handle audio-related stuffs. */ +public class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { private static final float AUDIO_MAX_VOLUME = 1.0f; private static final float AUDIO_MIN_VOLUME = 0.0f; private static final float AUDIO_DUCKING_VOLUME = 0.3f; @@ -32,42 +34,53 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { private final Activity mActivity; private final TunableTvViewPlayingApi mTvView; private final AudioManager mAudioManager; - private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; + @Nullable private final AudioFocusRequest mFocusRequest; - private boolean mAc3PassthroughSupported; - private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; + private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_NONE; - AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) { + public AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) { mActivity = activity; mTvView = tvView; mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE); - mAudioCapabilitiesReceiver = - new AudioCapabilitiesReceiver( - activity, - new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() { - @Override - public void onAc3PassthroughCapabilityChange(boolean capability) { - mAc3PassthroughSupported = capability; - } - }); - mAudioCapabilitiesReceiver.register(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mFocusRequest = + new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + .setAudioAttributes( + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build()) + .setOnAudioFocusChangeListener(this) + // Auto ducking from the system does not mute the TV Input Service. + // Using will pause when ducked allows us to set the stream volume + // even when we are not pausing. + .setWillPauseWhenDucked(true) + .build(); + } else { + mFocusRequest = null; + } } /** - * Sets suitable volume to {@link TunableTvView} according to the current audio focus. If the - * focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP mode, this - * method will finish the activity. + * Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current audio focus. + * + * <p>If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} or {@link + * AudioManager#AUDIOFOCUS_NONE} and the activity is under PIP mode, this method will finish the + * activity. Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current + * audio focus. If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is + * under PIP mode, this method will finish the activity. */ - void setVolumeByAudioFocusStatus() { + public void setVolumeByAudioFocusStatus() { if (mTvView.isPlaying()) { switch (mAudioFocusStatus) { case AudioManager.AUDIOFOCUS_GAIN: if (mTvView.isTimeShiftAvailable()) { - mTvView.timeshiftPlay(); + mTvView.timeShiftPlay(); } else { mTvView.setStreamVolume(AUDIO_MAX_VOLUME); } break; + case AudioManager.AUDIOFOCUS_NONE: case AudioManager.AUDIOFOCUS_LOSS: if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N @@ -78,14 +91,14 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { // fall through case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: if (mTvView.isTimeShiftAvailable()) { - mTvView.timeshiftPause(); + mTvView.timeShiftPause(); } else { mTvView.setStreamVolume(AUDIO_MIN_VOLUME); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: if (mTvView.isTimeShiftAvailable()) { - mTvView.timeshiftPause(); + mTvView.timeShiftPause(); } else { mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); } @@ -98,10 +111,15 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { * Tries to request audio focus from {@link AudioManager} and set volume according to the * returned result. */ - void requestAudioFocus() { - int result = - mAudioManager.requestAudioFocus( - this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + public void requestAudioFocus() { + int result; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + result = mAudioManager.requestAudioFocus(mFocusRequest); + } else { + result = + mAudioManager.requestAudioFocus( + this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? AudioManager.AUDIOFOCUS_GAIN @@ -110,19 +128,13 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { } /** Abandons audio focus. */ - void abandonAudioFocus() { + public void abandonAudioFocus() { mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; - mAudioManager.abandonAudioFocus(this); - } - - /** Returns {@code true} if the device supports AC3 pass-through. */ - boolean isAc3PassthroughSupported() { - return mAc3PassthroughSupported; - } - - /** Release the resources the helper class may occupied. */ - void release() { - mAudioCapabilitiesReceiver.unregister(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mAudioManager.abandonAudioFocusRequest(mFocusRequest); + } else { + mAudioManager.abandonAudioFocus(this); + } } @Override diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java new file mode 100644 index 00000000..5d0e9c82 --- /dev/null +++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 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.audiotvservice; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.media.session.MediaSession; +import android.net.Uri; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.util.Log; +import com.android.tv.data.ChannelImpl; +import com.android.tv.data.StreamInfo; +import com.android.tv.data.api.Channel; +import com.android.tv.ui.TunableTvView; +import com.android.tv.ui.TunableTvView.OnTuneListener; + +/** Foreground service for audio-only TV inputs. */ +public class AudioOnlyTvService extends Service implements OnTuneListener { + // TODO(b/110969180): implement this service. + private static final String TAG = "AudioOnlyTvService"; + private static final int NOTIFICATION_ID = 1; + + @Nullable private String mTvInputId; + private TunableTvView mTvView; + // TODO(b/110969180): perhaps use MediaSessionWrapper + private MediaSession mMediaSession; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + Log.i(TAG, "onBind"); + return null; + } + + @Override + public void onCreate() { + Log.i(TAG, "onCreate"); + // TODO(b/110969180): create TvView + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(TAG, "onStartCommand. flags = " + flags + ", startId = " + startId); + // TODO(b/110969180): real notification and or media session + startForeground(NOTIFICATION_ID, new Notification()); + mTvInputId = AudioOnlyTvServiceUtil.getInputIdFromIntent(intent); + tune(mTvInputId); + return START_STICKY; + } + + private void tune(String tvInputId) { + Channel channel = ChannelImpl.createPassthroughChannel(tvInputId); + mTvView.tuneTo(channel, null, this); + } + + @Override + public void onDestroy() { + Log.i(TAG, "onDestroy"); + mTvInputId = null; + // TODO(b/110969180): clear TvView + } + + // TODO(b/110969180): figure out when to stop ourselves, mediaSession event? + + // TODO(b/110969180): handle OnTuner Listener + @Override + public void onTuneFailed(Channel channel) {} + + @Override + public void onUnexpectedStop(Channel channel) {} + + @Override + public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {} + + @Override + public void onChannelRetuned(Uri channel) {} + + @Override + public void onContentBlocked() {} + + @Override + public void onContentAllowed() {} + + @Override + public void onChannelSignalStrength() {} +} diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java new file mode 100644 index 00000000..7ffe8833 --- /dev/null +++ b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 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.audiotvservice; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.util.Log; + +/** Utility methods to start and stop audio only TV Player. */ +public final class AudioOnlyTvServiceUtil { + private static final String TAG = "AudioOnlyTvServiceUtil"; + private static final String EXTRA_INPUT_ID = "intputId"; + + @MainThread + public static void startAudioOnlyInput(Context context, String tvInputId) { + Log.i(TAG, "startAudioOnlyInput"); + Intent intent = getIntent(context); + if (intent == null) { + return; + } + intent.putExtra(EXTRA_INPUT_ID, tvInputId); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } + + @Nullable + private static Intent getIntent(Context context) { + try { + return new Intent( + context, Class.forName("com.android.tv.audiotvservice.AudioOnlyTvService")); + } catch (ClassNotFoundException e) { + Log.wtf(TAG, e); + return null; + } + } + + @MainThread + public static void stopAudioOnlyInput(Context context) { + Log.i(TAG, "stopForegroundService"); + context.stopService(getIntent(context)); + } + + @Nullable + public static String getInputIdFromIntent(Intent intent) { + return intent.getStringExtra(EXTRA_INPUT_ID); + } + + private AudioOnlyTvServiceUtil() {} +} diff --git a/src/com/android/tv/audiotvservice/README.md b/src/com/android/tv/audiotvservice/README.md new file mode 100644 index 00000000..0f40ff6c --- /dev/null +++ b/src/com/android/tv/audiotvservice/README.md @@ -0,0 +1,18 @@ +# AudioOnlyTvServiceUtil + +This service plays audio only TV inputs in the "background". + + + +## Usage + +To start playing call + +```java +AudioOnlyTvServiceUtil.startAudioOnlyInput(context, tivInputServiceUri); +``` +To stop the playback call. + +```java +AudioOnlyTvServiceUtil.stopAudioOnlyInput(context); +```
\ No newline at end of file diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java index 0fb1e58d..9650fd18 100644 --- a/src/com/android/tv/data/BaseProgram.java +++ b/src/com/android/tv/data/BaseProgram.java @@ -21,7 +21,9 @@ import android.media.tv.TvContentRating; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.R; +import com.google.common.collect.ImmutableList; import java.util.Comparator; +import java.util.Objects; /** * Base class for {@link com.android.tv.data.Program} and {@link @@ -43,6 +45,10 @@ public abstract class BaseProgram { public static final Comparator<BaseProgram> SEASON_REVERSED_EPISODE_COMPARATOR = new EpisodeComparator(true); + public static final String COLUMN_SERIES_ID = "series_id"; + + public static final String COLUMN_STATE = "state"; + private static class EpisodeComparator implements Comparator<BaseProgram> { private final boolean mReversedSeason; @@ -66,7 +72,7 @@ public abstract class BaseProgram { /** Compares two strings represent season numbers or episode numbers of programs. */ public static int numberCompare(String s1, String s2) { - if (s1 == s2) { + if (Objects.equals(s1, s2)) { return 0; } else if (s1 == null) { return -1; @@ -92,6 +98,7 @@ public abstract class BaseProgram { public abstract String getEpisodeTitle(); /** Returns the displayed title of the program episode. */ + @Nullable public String getEpisodeDisplayTitle(Context context) { String episodeNumber = getEpisodeNumber(); String episodeTitle = getEpisodeTitle(); @@ -162,6 +169,7 @@ public abstract class BaseProgram { public abstract long getDurationMillis(); /** Returns the series ID. */ + @Nullable public abstract String getSeriesId(); /** Returns the season number. */ @@ -180,8 +188,7 @@ public abstract class BaseProgram { public abstract int[] getCanonicalGenreIds(); /** Returns the array of content ratings. */ - @Nullable - public abstract TvContentRating[] getContentRatings(); + public abstract ImmutableList<TvContentRating> getContentRatings(); /** Returns channel's ID of the program. */ public abstract long getChannelId(); diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 1dfcf125..a5c786cf 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -23,7 +23,6 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; -import android.database.sqlite.SQLiteException; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputManager.TvInputCallback; @@ -47,7 +46,7 @@ import com.android.tv.data.api.Channel; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; -import java.io.IOException; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -515,7 +514,7 @@ public class ChannelDataManager { if (mChannelsUpdateTask != null) { mChannelsUpdateTask.cancel(true); } - mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver); + mChannelsUpdateTask = new QueryAllChannelsTask(); mChannelsUpdateTask.executeOnDbThread(); } @@ -599,8 +598,10 @@ public class ChannelDataManager { .openAssetFileDescriptor( TvContract.buildChannelLogoUri(mChannel.getId()), "r")) { return true; - } catch (SQLiteException | IOException | NullPointerException e) { - // File not found or asset file not found. + } catch (FileNotFoundException e) { + // no need to log just return false + } catch (Exception e) { + Log.w(TAG, "Unable to find logo for " + mChannel, e); } return false; } @@ -616,8 +617,8 @@ public class ChannelDataManager { private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { - QueryAllChannelsTask(ContentResolver contentResolver) { - super(mDbExecutor, contentResolver); + QueryAllChannelsTask() { + super(mDbExecutor, mContext); } @Override @@ -736,15 +737,12 @@ public class ChannelDataManager { return; } mDbExecutor.execute( - new Runnable() { - @Override - public void run() { - String selection = Utils.buildSelectionForIds(Channels._ID, ids); - ContentValues values = new ContentValues(); - values.put(columnName, columnValue); - mContentResolver.update( - TvContract.Channels.CONTENT_URI, values, selection, null); - } + () -> { + String selection = Utils.buildSelectionForIds(Channels._ID, ids); + ContentValues values = new ContentValues(); + values.put(columnName, columnValue); + mContentResolver.update( + TvContract.Channels.CONTENT_URI, values, selection, null); }); } diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java index 703f69c9..f31290d0 100644 --- a/src/com/android/tv/data/ChannelImpl.java +++ b/src/com/android/tv/data/ChannelImpl.java @@ -46,12 +46,8 @@ public final class ChannelImpl implements Channel { /** Compares the channel numbers of channels which belong to the same input. */ public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = - new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } - }; + (Channel lhs, Channel rhs) -> + ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); private static final int APP_LINK_TYPE_NOT_SET = 0; private static final String INVALID_PACKAGE_NAME = "packageName"; @@ -74,6 +70,7 @@ public final class ChannelImpl implements Channel { TvContract.Channels.COLUMN_APP_LINK_ICON_URI, TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, + TvContract.Channels.COLUMN_NETWORK_AFFILIATION, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input }; @@ -102,6 +99,7 @@ public final class ChannelImpl implements Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); + channel.mNetworkAffiliation = cursor.getString(index++); if (CommonUtils.isBundledInput(channel.mInputId)) { channel.mRecordingProhibited = cursor.getInt(index++) != 0; } @@ -146,6 +144,7 @@ public final class ChannelImpl implements Channel { private String mAppLinkPosterArtUri; private String mAppLinkIntentUri; private Intent mAppLinkIntent; + private String mNetworkAffiliation; private int mAppLinkType; private String mLogoUri; private boolean mRecordingProhibited; @@ -247,6 +246,11 @@ public final class ChannelImpl implements Channel { return mAppLinkIntentUri; } + @Override + public String getNetworkAffiliation() { + return mNetworkAffiliation; + } + /** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */ @Override public String getLogoUri() { @@ -311,6 +315,11 @@ public final class ChannelImpl implements Channel { mLogoUri = logoUri; } + @Override + public void setNetworkAffiliation(String networkAffiliation) { + mNetworkAffiliation = networkAffiliation; + } + /** * Check whether {@code other} has same read-only channel info as this. But, it cannot check two * channels have same logos. It also excludes browsable and locked, because two fields are @@ -393,8 +402,10 @@ public final class ChannelImpl implements Channel { mAppLinkIconUri = channel.getAppLinkIconUri(); mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri(); mAppLinkIntentUri = channel.getAppLinkIntentUri(); + mNetworkAffiliation = channel.getNetworkAffiliation(); mRecordingProhibited = channel.isRecordingProhibited(); mChannelLogoExist = channel.channelLogoExists(); + mNetworkAffiliation = channel.getNetworkAffiliation(); } } @@ -421,6 +432,7 @@ public final class ChannelImpl implements Channel { mAppLinkIconUri = other.mAppLinkIconUri; mAppLinkPosterArtUri = other.mAppLinkPosterArtUri; mAppLinkIntentUri = other.mAppLinkIntentUri; + mNetworkAffiliation = channel.mNetworkAffiliation; mAppLinkIntent = other.mAppLinkIntent; mAppLinkType = other.mAppLinkType; mRecordingProhibited = other.mRecordingProhibited; @@ -543,6 +555,12 @@ public final class ChannelImpl implements Channel { return this; } + @VisibleForTesting + public Builder setNetworkAffiliation(String networkAffiliation) { + mChannel.mNetworkAffiliation = networkAffiliation; + return this; + } + public Builder setAppLinkColor(int appLinkColor) { mChannel.mAppLinkColor = appLinkColor; return this; diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java index 44664dcf..8616aeec 100644 --- a/src/com/android/tv/data/PreviewDataManager.java +++ b/src/com/android/tv/data/PreviewDataManager.java @@ -21,7 +21,6 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; -import android.database.SQLException; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; @@ -31,10 +30,10 @@ import android.os.AsyncTask; import android.os.Build; import android.support.annotation.IntDef; import android.support.annotation.MainThread; -import android.support.media.tv.ChannelLogoUtils; -import android.support.media.tv.PreviewProgram; import android.util.Log; import android.util.Pair; +import androidx.tvprovider.media.tv.ChannelLogoUtils; +import androidx.tvprovider.media.tv.PreviewProgram; import com.android.tv.R; import com.android.tv.common.util.PermissionUtils; import java.lang.annotation.Retention; @@ -225,14 +224,14 @@ public class PreviewDataManager { try (Cursor cursor = mContentResolver.query( previewChannelsUri, - android.support.media.tv.Channel.PROJECTION, + androidx.tvprovider.media.tv.Channel.PROJECTION, mChannelSelection, new String[] {packageName}, null)) { if (cursor != null) { while (cursor.moveToNext()) { - android.support.media.tv.Channel previewChannel = - android.support.media.tv.Channel.fromCursor(cursor); + androidx.tvprovider.media.tv.Channel previewChannel = + androidx.tvprovider.media.tv.Channel.fromCursor(cursor); Long previewChannelType = previewChannel.getInternalProviderFlag1(); if (previewChannelType != null) { previewData.addPreviewChannelId( @@ -245,14 +244,14 @@ public class PreviewDataManager { try (Cursor cursor = mContentResolver.query( previewChannelsUri, - android.support.media.tv.Channel.PROJECTION, + androidx.tvprovider.media.tv.Channel.PROJECTION, null, null, null)) { if (cursor != null) { while (cursor.moveToNext()) { - android.support.media.tv.Channel previewChannel = - android.support.media.tv.Channel.fromCursor(cursor); + androidx.tvprovider.media.tv.Channel previewChannel = + androidx.tvprovider.media.tv.Channel.fromCursor(cursor); Long previewChannelType = previewChannel.getInternalProviderFlag1(); if (packageName.equals(previewChannel.getPackageName()) && previewChannelType != null) { @@ -283,7 +282,7 @@ public class PreviewDataManager { } } } - } catch (SQLException e) { + } catch (Exception e) { Log.w(TAG, "Unable to get preview data", e); } return previewData; @@ -554,7 +553,7 @@ public class PreviewDataManager { /** A utils class for preview data. */ public static final class PreviewDataUtils { /** Creates a preview channel. */ - public static android.support.media.tv.Channel createPreviewChannel( + public static androidx.tvprovider.media.tv.Channel createPreviewChannel( Context context, @PreviewChannelType long previewChannelType) { if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) { return createRecordedProgramPreviewChannel(context, previewChannelType); @@ -562,10 +561,10 @@ public class PreviewDataManager { return createDefaultPreviewChannel(context, previewChannelType); } - private static android.support.media.tv.Channel createDefaultPreviewChannel( + private static androidx.tvprovider.media.tv.Channel createDefaultPreviewChannel( Context context, @PreviewChannelType long previewChannelType) { - android.support.media.tv.Channel.Builder builder = - new android.support.media.tv.Channel.Builder(); + androidx.tvprovider.media.tv.Channel.Builder builder = + new androidx.tvprovider.media.tv.Channel.Builder(); CharSequence appLabel = context.getApplicationInfo().loadLabel(context.getPackageManager()); CharSequence appDescription = @@ -578,10 +577,10 @@ public class PreviewDataManager { return builder.build(); } - private static android.support.media.tv.Channel createRecordedProgramPreviewChannel( + private static androidx.tvprovider.media.tv.Channel createRecordedProgramPreviewChannel( Context context, @PreviewChannelType long previewChannelType) { - android.support.media.tv.Channel.Builder builder = - new android.support.media.tv.Channel.Builder(); + androidx.tvprovider.media.tv.Channel.Builder builder = + new androidx.tvprovider.media.tv.Channel.Builder(); builder.setType(TvContract.Channels.TYPE_PREVIEW) .setDisplayName( context.getResources() diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java index b5156408..8d4b88cf 100644 --- a/src/com/android/tv/data/PreviewProgramContent.java +++ b/src/com/android/tv/data/PreviewProgramContent.java @@ -19,9 +19,9 @@ package com.android.tv.data; import android.content.Context; import android.net.Uri; import android.support.annotation.VisibleForTesting; -import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Pair; +import androidx.tvprovider.media.tv.TvContractCompat; import com.android.tv.TvSingletons; import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 2c64cdbb..b688927a 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -30,6 +30,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; import com.android.tv.common.BuildConfig; @@ -37,8 +38,10 @@ import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.util.CollectionUtils; import com.android.tv.common.util.CommonUtils; import com.android.tv.data.api.Channel; +import com.android.tv.util.TvProviderUtils; import com.android.tv.util.Utils; import com.android.tv.util.images.ImageLoader; +import com.google.common.collect.ImmutableList; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; @@ -86,6 +89,16 @@ public final class Program extends BaseProgram implements Comparable<Program>, P public static final String[] PROJECTION = createProjection(); + public static final String[] PARTIAL_PROJECTION = { + TvContract.Programs._ID, + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.Programs.COLUMN_TITLE, + TvContract.Programs.COLUMN_EPISODE_TITLE, + TvContract.Programs.COLUMN_CANONICAL_GENRE, + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, + }; + private static String[] createProjection() { return CollectionUtils.concatAll( PROJECTION_BASE, @@ -94,7 +107,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P : PROJECTION_DEPRECATED_IN_NYC); } - /** Returns the column index for {@code column}, -1 if the column doesn't exist. */ + /** + * Returns the column index for {@code column},-1 if the column doesn't exist in {@link + * #PROJECTION}. + */ public static int getColumnIndex(String column) { for (int i = 0; i < PROJECTION.length; ++i) { if (PROJECTION[i].equals(column)) { @@ -104,11 +120,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return -1; } - /** - * Creates {@code Program} object from cursor. - * - * <p>The query that created the cursor MUST use {@link #PROJECTION}. - */ + /** Creates {@code Program} object from cursor. */ public static Program fromCursor(Cursor cursor) { // Columns read must match the order of match {@link #PROJECTION} Builder builder = new Builder(); @@ -143,6 +155,27 @@ public final class Program extends BaseProgram implements Comparable<Program>, P builder.setSeasonNumber(cursor.getString(index++)); builder.setEpisodeNumber(cursor.getString(index++)); } + if (TvProviderUtils.getProgramHasSeriesIdColumn()) { + String seriesId = cursor.getString(index); + if (!TextUtils.isEmpty(seriesId)) { + builder.setSeriesId(seriesId); + } + } + return builder.build(); + } + + /** Creates {@code Program} object from cursor. */ + public static Program fromCursorPartialProjection(Cursor cursor) { + // Columns read must match the order of match {@link #PARTIAL_PROJECTION} + Builder builder = new Builder(); + int index = 0; + builder.setId(cursor.getLong(index++)); + builder.setChannelId(cursor.getLong(index++)); + builder.setTitle(cursor.getString(index++)); + builder.setEpisodeTitle(cursor.getString(index++)); + builder.setCanonicalGenres(cursor.getString(index++)); + builder.setStartTimeUtcMillis(cursor.getLong(index++)); + builder.setEndTimeUtcMillis(cursor.getLong(index++)); return builder.build(); } @@ -169,10 +202,14 @@ public final class Program extends BaseProgram implements Comparable<Program>, P program.mCanonicalGenreIds = in.createIntArray(); int length = in.readInt(); if (length > 0) { - program.mContentRatings = new TvContentRating[length]; + ImmutableList.Builder<TvContentRating> ratingsBuilder = + ImmutableList.builderWithExpectedSize(length); for (int i = 0; i < length; ++i) { - program.mContentRatings[i] = TvContentRating.unflattenFromString(in.readString()); + ratingsBuilder.add(TvContentRating.unflattenFromString(in.readString())); } + program.mContentRatings = ratingsBuilder.build(); + } else { + program.mContentRatings = ImmutableList.of(); } program.mRecordingProhibited = in.readByte() != (byte) 0; return program; @@ -202,6 +239,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P private String mEpisodeNumber; private long mStartTimeUtcMillis; private long mEndTimeUtcMillis; + private String mDurationString; private String mDescription; private String mLongDescription; private int mVideoWidth; @@ -210,7 +248,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P private String mPosterArtUri; private String mThumbnailUri; private int[] mCanonicalGenreIds; - private TvContentRating[] mContentRatings; + private ImmutableList<TvContentRating> mContentRatings; private boolean mRecordingProhibited; private Program() { @@ -278,6 +316,15 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mEndTimeUtcMillis; } + public String getDurationString(Context context) { + // TODO(b/71717446): expire the calculated string + if (mDurationString == null) { + mDurationString = + Utils.getDurationString(context, mStartTimeUtcMillis, mEndTimeUtcMillis, true); + } + return mDurationString; + } + /** Returns the program duration. */ @Override public long getDurationMillis() { @@ -310,7 +357,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P @Nullable @Override - public TvContentRating[] getContentRatings() { + public ImmutableList<TvContentRating> getContentRatings() { return mContentRatings; } @@ -379,7 +426,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P mVideoHeight, mPosterArtUri, mThumbnailUri, - Arrays.hashCode(mContentRatings), + mContentRatings, Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, @@ -407,7 +454,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P && mVideoHeight == program.mVideoHeight && Objects.equals(mPosterArtUri, program.mPosterArtUri) && Objects.equals(mThumbnailUri, program.mThumbnailUri) - && Arrays.equals(mContentRatings, program.mContentRatings) + && Objects.equals(mContentRatings, program.mContentRatings) && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds) && Objects.equals(mSeasonNumber, program.mSeasonNumber) && Objects.equals(mSeasonTitle, program.mSeasonTitle) @@ -474,7 +521,8 @@ public final class Program extends BaseProgram implements Comparable<Program>, P */ @SuppressLint("InlinedApi") @SuppressWarnings("deprecation") - public static ContentValues toContentValues(Program program) { + @WorkerThread + public static ContentValues toContentValues(Program program, Context context) { ContentValues values = new ContentValues(); values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); if (!TextUtils.isEmpty(program.getPackageName())) { @@ -495,6 +543,10 @@ public final class Program extends BaseProgram implements Comparable<Program>, P putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); } + if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) { + putValue(values, COLUMN_SERIES_ID, program.getSeriesId()); + } + putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); @@ -554,6 +606,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P mEpisodeNumber = other.mEpisodeNumber; mStartTimeUtcMillis = other.mStartTimeUtcMillis; mEndTimeUtcMillis = other.mEndTimeUtcMillis; + mDurationString = null; // Recreate Duration when needed. mDescription = other.mDescription; mLongDescription = other.mLongDescription; mVideoWidth = other.mVideoWidth; @@ -582,6 +635,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P mProgram.mEpisodeNumber = null; mProgram.mStartTimeUtcMillis = -1; mProgram.mEndTimeUtcMillis = -1; + mProgram.mDurationString = null; mProgram.mDescription = null; mProgram.mLongDescription = null; mProgram.mRecordingProhibited = false; @@ -771,7 +825,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P * @param contentRatings the content ratings * @return a reference to this object */ - public Builder setContentRatings(TvContentRating[] contentRatings) { + public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) { mProgram.mContentRatings = contentRatings; return this; } @@ -947,7 +1001,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P out.writeString(mPosterArtUri); out.writeString(mThumbnailUri); out.writeIntArray(mCanonicalGenreIds); - out.writeInt(mContentRatings == null ? 0 : mContentRatings.length); + out.writeInt(mContentRatings == null ? 0 : mContentRatings.size()); if (mContentRatings != null) { for (TvContentRating rating : mContentRatings) { out.writeString(rating.flattenToString()); diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index 4631806c..2f20c89a 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -35,14 +35,17 @@ import android.util.LongSparseArray; import android.util.LruCache; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.config.api.RemoteConfig; -import com.android.tv.common.config.api.RemoteConfigValue; import com.android.tv.common.memory.MemoryManageable; import com.android.tv.common.util.Clock; import com.android.tv.data.api.Channel; +import com.android.tv.perf.EventNames; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.TimerEvent; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.MultiLongSparseArray; +import com.android.tv.util.TvProviderUtils; import com.android.tv.util.Utils; +import com.android.tv.common.flags.BackendKnobsFlags; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -71,8 +74,6 @@ public class ProgramDataManager implements MemoryManageable { // TODO: need to optimize consecutive DB updates. private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); @VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30); - private static final RemoteConfigValue<Long> PROGRAM_GUIDE_MAX_HOURS = - RemoteConfigValue.create("live_channels_program_guide_max_hours", 48); // TODO: Use TvContract constants, once they become public. private static final String PARAM_START_TIME = "start_time"; @@ -90,10 +91,13 @@ public class ProgramDataManager implements MemoryManageable { private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001; private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002; + private final Context mContext; private final Clock mClock; private final ContentResolver mContentResolver; private final Executor mDbExecutor; - private final RemoteConfig mRemoteConfig; + private final BackendKnobsFlags mBackendKnobsFlags; + private final PerformanceMonitor mPerformanceMonitor; + private final ChannelDataManager mChannelDataManager; private boolean mStarted; // Updated only on the main thread. private volatile boolean mCurrentProgramsLoadFinished; @@ -104,15 +108,15 @@ public class ProgramDataManager implements MemoryManageable { private final MultiLongSparseArray<OnCurrentProgramUpdatedListener> mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>(); private final Handler mHandler; - private final Set<Listener> mListeners = new ArraySet<>(); - + private final Set<Callback> mCallbacks = new ArraySet<>(); + private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>(); + private final Set<Long> mCompleteInfoChannelIds = new HashSet<>(); private final ContentObserver mProgramObserver; private boolean mPrefetchEnabled; private long mProgramPrefetchUpdateWaitMs; private long mLastPrefetchTaskRunMs; private ProgramsPrefetchTask mProgramsPrefetchTask; - private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>(); // Any program that ends prior to this time will be removed from the cache // when a channel's current program is updated. @@ -125,25 +129,34 @@ public class ProgramDataManager implements MemoryManageable { @MainThread public ProgramDataManager(Context context) { this( + context, TvSingletons.getSingletons(context).getDbExecutor(), context.getContentResolver(), Clock.SYSTEM, Looper.myLooper(), - TvSingletons.getSingletons(context).getRemoteConfig()); + TvSingletons.getSingletons(context).getBackendKnobs(), + TvSingletons.getSingletons(context).getPerformanceMonitor(), + TvSingletons.getSingletons(context).getChannelDataManager()); } @VisibleForTesting ProgramDataManager( + Context context, Executor executor, ContentResolver contentResolver, Clock time, Looper looper, - RemoteConfig remoteConfig) { + BackendKnobsFlags backendKnobsFlags, + PerformanceMonitor performanceMonitor, + ChannelDataManager channelDataManager) { + mContext = context; mDbExecutor = executor; mClock = time; mContentResolver = contentResolver; mHandler = new MyHandler(looper); - mRemoteConfig = remoteConfig; + mBackendKnobsFlags = backendKnobsFlags; + mPerformanceMonitor = performanceMonitor; + mChannelDataManager = channelDataManager; mProgramObserver = new ContentObserver(mHandler) { @Override @@ -246,24 +259,43 @@ public class ProgramDataManager implements MemoryManageable { } } - /** A listener interface to receive notification on program data retrieval from DB. */ - public interface Listener { + public void prefetchChannel(long channelId) { + if (mCompleteInfoChannelIds.add(channelId)) { + long startTimeMs = + Utils.floorTime( + mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS, + PROGRAM_GUIDE_SNAP_TIME_MS); + long endTimeMs = startTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration()); + new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread(); + } + } + + /** A Callback interface to receive notification on program data retrieval from DB. */ + public interface Callback { /** * Called when a Program data is now available through getProgram() after the DB operation * is done which wasn't before. This would be called only if fetched data is around the * selected program. */ void onProgramUpdated(); + + /** + * Called when we update complete program data of specific channel during scrolling. Data is + * loaded from DB on request basis. + * + * @param channelId + */ + void onSingleChannelUpdated(long channelId); } - /** Adds the {@link Listener}. */ - public void addListener(Listener listener) { - mListeners.add(listener); + /** Adds the {@link Callback}. */ + public void addCallback(Callback callback) { + mCallbacks.add(callback); } - /** Removes the {@link Listener}. */ - public void removeListener(Listener listener) { - mListeners.remove(listener); + /** Removes the {@link Callback}. */ + public void removeCallback(Callback callback) { + mCallbacks.remove(callback); } /** Enables or Disables program prefetch. */ @@ -451,7 +483,7 @@ public class ProgramDataManager implements MemoryManageable { } clearTask(mProgramUpdateTaskMap); mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM); - mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis()); + mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis()); mProgramsUpdateTask.executeOnDbThread(); } @@ -461,20 +493,29 @@ public class ProgramDataManager implements MemoryManageable { private final long mEndTimeMs; private boolean mSuccess; + private TimerEvent mFromEmptyCacheTimeEvent; public ProgramsPrefetchTask() { super(mDbExecutor); long time = mClock.currentTimeMillis(); mStartTimeMs = Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS); - mEndTimeMs = - mStartTimeMs - + TimeUnit.HOURS.toMillis(PROGRAM_GUIDE_MAX_HOURS.get(mRemoteConfig)); + mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration()); mSuccess = false; } @Override + protected void onPreExecute() { + if (mChannelIdCurrentProgramMap.isEmpty()) { + // No current program guide is shown. + // Measure the delay before users can see program guides. + mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer(); + } + } + + @Override protected Map<Long, ArrayList<Program>> doInBackground(Void... params) { + TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer(); Map<Long, ArrayList<Program>> programMap = new HashMap<>(); if (DEBUG) { Log.d( @@ -497,8 +538,19 @@ public class ProgramDataManager implements MemoryManageable { return null; } programMap.clear(); - try (Cursor c = - mContentResolver.query(uri, Program.PROJECTION, null, null, SORT_BY_TIME)) { + + String[] projection = + mBackendKnobsFlags.enablePartialProgramFetch() + ? Program.PARTIAL_PROJECTION + : Program.PROJECTION; + if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) { + if (Utils.isProgramsUri(uri)) { + projection = + TvProviderUtils.addExtraColumnsToProjection( + projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); + } + } + try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) { if (c == null) { continue; } @@ -510,7 +562,10 @@ public class ProgramDataManager implements MemoryManageable { } return null; } - Program program = Program.fromCursor(c); + Program program = + mBackendKnobsFlags.enablePartialProgramFetch() + ? Program.fromCursorPartialProjection(c) + : Program.fromCursor(c); if (Program.isDuplicate(program, lastReadProgram)) { duplicateCount++; continue; @@ -520,6 +575,15 @@ public class ProgramDataManager implements MemoryManageable { ArrayList<Program> programs = programMap.get(program.getChannelId()); if (programs == null) { programs = new ArrayList<>(); + if (mBackendKnobsFlags.enablePartialProgramFetch()) { + // To skip already loaded complete data. + Program currentProgramInfo = + mChannelIdCurrentProgramMap.get(program.getChannelId()); + if (currentProgramInfo != null + && Program.isDuplicate(program, currentProgramInfo)) { + program = currentProgramInfo; + } + } programMap.put(program.getChannelId(), programs); } programs.add(program); @@ -534,12 +598,17 @@ public class ProgramDataManager implements MemoryManageable { Log.d(TAG, "Database is changed while querying. Will retry."); } } catch (SecurityException e) { - Log.d(TAG, "Security exception during program data query", e); + Log.w(TAG, "Security exception during program data query", e); + } catch (Exception e) { + Log.w(TAG, "Error during program data query", e); } } if (DEBUG) { Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels"); } + mPerformanceMonitor.stopTimer( + asyncTimeEvent, + EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND); return programMap; } @@ -552,8 +621,6 @@ public class ProgramDataManager implements MemoryManageable { } long nextMessageDelayedTime; if (mSuccess) { - mChannelIdProgramCache = programs; - notifyProgramUpdated(); long currentTime = mClock.currentTimeMillis(); mLastPrefetchTaskRunMs = currentTime; nextMessageDelayedTime = @@ -561,6 +628,22 @@ public class ProgramDataManager implements MemoryManageable { mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS) - currentTime; + // Issue second pre-fetch immediately after the first partial update + if (mChannelIdProgramCache.isEmpty()) { + nextMessageDelayedTime = 0; + } + mChannelIdProgramCache = programs; + if (mBackendKnobsFlags.enablePartialProgramFetch()) { + // Since cache has partial data we need to reset the map of complete data. + mCompleteInfoChannelIds.clear(); + } + notifyProgramUpdated(); + if (mFromEmptyCacheTimeEvent != null) { + mPerformanceMonitor.stopTimer( + mFromEmptyCacheTimeEvent, + EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE); + mFromEmptyCacheTimeEvent = null; + } } else { nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS; } @@ -571,17 +654,78 @@ public class ProgramDataManager implements MemoryManageable { } } + private long getFetchDuration() { + if (mChannelIdProgramCache.isEmpty()) { + return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours()); + } else { + long durationHours; + int channelCount = mChannelDataManager.getChannelCount(); + long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours(); + long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount(); + if (channelCount <= targetChannelCount) { + durationHours = Math.max(48L, knobsMaxHours); + } else { + // 2 days <= duration <= 14 days (336 hours) + durationHours = knobsMaxHours * targetChannelCount / channelCount; + if (durationHours < 48L) { + durationHours = 48L; + } else if (durationHours > 336L) { + durationHours = 336L; + } + } + return durationHours; + } + } + + private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> { + long mChannelId; + + public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) { + super( + mDbExecutor, + mContext, + TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs), + Program.PROJECTION, + null, + null, + SORT_BY_TIME); + mChannelId = channelId; + } + + @Override + protected ArrayList<Program> onQuery(Cursor c) { + ArrayList<Program> programMap = new ArrayList<>(); + while (c.moveToNext()) { + Program program = Program.fromCursor(c); + programMap.add(program); + } + return programMap; + } + + @Override + protected void onPostExecute(ArrayList<Program> programs) { + mChannelIdProgramCache.put(mChannelId, programs); + notifySingleChannelUpdated(mChannelId); + } + } + private void notifyProgramUpdated() { - for (Listener listener : mListeners) { - listener.onProgramUpdated(); + for (Callback callback : mCallbacks) { + callback.onProgramUpdated(); + } + } + + private void notifySingleChannelUpdated(long channelId) { + for (Callback callback : mCallbacks) { + callback.onSingleChannelUpdated(channelId); } } private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> { - public ProgramsUpdateTask(ContentResolver contentResolver, long time) { + public ProgramsUpdateTask(long time) { super( mDbExecutor, - contentResolver, + mContext, Programs.CONTENT_URI .buildUpon() .appendQueryParameter(PARAM_START_TIME, String.valueOf(time)) @@ -633,6 +777,9 @@ public class ProgramDataManager implements MemoryManageable { for (Long channelId : removedChannelIds) { if (mPrefetchEnabled) { mChannelIdProgramCache.remove(channelId); + if (mBackendKnobsFlags.enablePartialProgramFetch()) { + mCompleteInfoChannelIds.remove(channelId); + } } mChannelIdCurrentProgramMap.remove(channelId); notifyCurrentProgramUpdate(channelId, null); @@ -645,11 +792,10 @@ public class ProgramDataManager implements MemoryManageable { private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> { private final long mChannelId; - private UpdateCurrentProgramForChannelTask( - ContentResolver contentResolver, long channelId, long time) { + private UpdateCurrentProgramForChannelTask(long channelId, long time) { super( mDbExecutor, - contentResolver, + mContext, TvContract.buildProgramsUriForChannel(channelId, time, time), Program.PROJECTION, null, @@ -695,7 +841,7 @@ public class ProgramDataManager implements MemoryManageable { } UpdateCurrentProgramForChannelTask task = new UpdateCurrentProgramForChannelTask( - mContentResolver, channelId, mClock.currentTimeMillis()); + channelId, mClock.currentTimeMillis()); mProgramUpdateTaskMap.put(channelId, task); task.executeOnDbThread(); break; diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java index 7187efd1..9c1d423f 100644 --- a/src/com/android/tv/data/WatchedHistoryManager.java +++ b/src/com/android/tv/data/WatchedHistoryManager.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Scanner; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; /** @@ -73,24 +74,20 @@ public class WatchedHistoryManager { // onNewRecordAdded will be called in the same thread as the thread // which created this instance. mHandler.post( - new Runnable() { - @Override - public void run() { - for (long i = mLastIndex + 1; i <= lastIndex; ++i) { - WatchedRecord record = - decode( - mSharedPreferences.getString( - getSharedPreferencesKey(i), - null)); - if (record != null) { - mWatchedHistory.add(record); - if (mListener != null) { - mListener.onNewRecordAdded(record); - } + () -> { + for (long i = mLastIndex + 1; i <= lastIndex; ++i) { + WatchedRecord record = + decode( + mSharedPreferences.getString( + getSharedPreferencesKey(i), null)); + if (record != null) { + mWatchedHistory.add(record); + if (mListener != null) { + mListener.onNewRecordAdded(record); } } - mLastIndex = lastIndex; } + mLastIndex = lastIndex; }); } } @@ -100,16 +97,18 @@ public class WatchedHistoryManager { private Listener mListener; private final int mMaxHistorySize; private final Handler mHandler; + private final Executor mExecutor; public WatchedHistoryManager(Context context) { - this(context, MAX_HISTORY_SIZE); + this(context, MAX_HISTORY_SIZE, AsyncTask.THREAD_POOL_EXECUTOR); } @VisibleForTesting - WatchedHistoryManager(Context context, int maxHistorySize) { + WatchedHistoryManager(Context context, int maxHistorySize, Executor executor) { mContext = context.getApplicationContext(); mMaxHistorySize = maxHistorySize; mHandler = new Handler(); + mExecutor = executor; } /** Starts the manager. It loads history data from {@link SharedPreferences}. */ @@ -130,7 +129,7 @@ public class WatchedHistoryManager { protected void onPostExecute(Void params) { onLoadFinished(); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }.executeOnExecutor(mExecutor); } else { loadWatchedHistory(); onLoadFinished(); diff --git a/src/com/android/tv/data/api/Channel.java b/src/com/android/tv/data/api/Channel.java index 496331cf..fb00952c 100644 --- a/src/com/android/tv/data/api/Channel.java +++ b/src/com/android/tv/data/api/Channel.java @@ -85,6 +85,8 @@ public interface Channel { String getAppLinkIntentUri(); + String getNetworkAffiliation(); + String getLogoUri(); boolean isRecordingProhibited(); @@ -109,6 +111,8 @@ public interface Channel { void setLogoUri(String logoUri); + void setNetworkAffiliation(String networkAffiliation); + boolean channelLogoExists(); void loadBitmap( diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java deleted file mode 100644 index 795ad5c4..00000000 --- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2018 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.data.epg; - -import com.android.tv.data.api.Channel; - -/** - * Hand copy of generated Autovalue class. - * - * TODO get autovalue working - */ -final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel { - - private final Channel channel; - private final String epgChannelId; - - AutoValue_EpgReader_EpgChannel( - Channel channel, - String epgChannelId) { - if (channel == null) { - throw new NullPointerException("Null channel"); - } - this.channel = channel; - if (epgChannelId == null) { - throw new NullPointerException("Null epgChannelId"); - } - this.epgChannelId = epgChannelId; - } - - @Override - public Channel getChannel() { - return channel; - } - - @Override - public String getEpgChannelId() { - return epgChannelId; - } - - @Override - public String toString() { - return "EpgChannel{" - + "channel=" + channel + ", " - + "epgChannelId=" + epgChannelId - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof EpgReader.EpgChannel) { - EpgReader.EpgChannel that = (EpgReader.EpgChannel) o; - return (this.channel.equals(that.getChannel())) - && (this.epgChannelId.equals(that.getEpgChannelId())); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.channel.hashCode(); - h *= 1000003; - h ^= this.epgChannelId.hashCode(); - return h; - } - -} - diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java index 3c7112ec..3843ca99 100644 --- a/src/com/android/tv/data/epg/EpgFetchHelper.java +++ b/src/com/android/tv/data/epg/EpgFetchHelper.java @@ -17,6 +17,7 @@ package com.android.tv.data.epg; import android.content.ContentProviderOperation; +import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; @@ -30,9 +31,13 @@ import android.util.Log; import com.android.tv.common.CommonConstants; import com.android.tv.common.util.Clock; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.android.tv.features.TvFeatures; +import com.android.tv.util.TvProviderUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; /** The helper class for {@link EpgFetcher} */ @@ -101,7 +106,7 @@ class EpgFetchHelper { ops.add( ContentProviderOperation.newUpdate( TvContract.buildProgramUri(oldProgram.getId())) - .withValues(Program.toContentValues(newProgram)) + .withValues(Program.toContentValues(newProgram, context)) .build()); oldProgramsIndex++; newProgramsIndex++; @@ -127,7 +132,7 @@ class EpgFetchHelper { if (addNewProgram) { ops.add( ContentProviderOperation.newInsert(Programs.CONTENT_URI) - .withValues(Program.toContentValues(newProgram)) + .withValues(Program.toContentValues(newProgram, context)) .build()); } // Throttle the batch operation not to cause TransactionTooLargeException. @@ -155,14 +160,57 @@ class EpgFetchHelper { return updated; } + @WorkerThread + static void updateNetworkAffiliation(Context context, Set<EpgReader.EpgChannel> channels) { + if (!TvFeatures.STORE_NETWORK_AFFILIATION.isEnabled(context)) { + return; + } + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); + for (EpgReader.EpgChannel epgChannel : channels) { + if (!epgChannel.getDbUpdateNeeded()) { + continue; + } + Channel channel = epgChannel.getChannel(); + + ContentValues values = new ContentValues(); + values.put( + TvContract.Channels.COLUMN_NETWORK_AFFILIATION, + channel.getNetworkAffiliation()); + ops.add( + ContentProviderOperation.newUpdate(TvContract.buildChannelUri(channel.getId())) + .withValues(values) + .build()); + if (ops.size() >= BATCH_OPERATION_COUNT) { + try { + context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Failed to update channels.", e); + } + ops.clear(); + } + } + try { + context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Failed to update channels.", e); + } + } + + @WorkerThread private static List<Program> queryPrograms( Context context, long channelId, long startTimeMs, long endTimeMs) { + String[] projection = Program.PROJECTION; + if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) { + projection = + TvProviderUtils.addExtraColumnsToProjection( + projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); + } try (Cursor c = context.getContentResolver() .query( TvContract.buildProgramsUriForChannel( channelId, startTimeMs, endTimeMs), - Program.PROJECTION, + projection, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) { diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java index 2aaaa5b2..b191421f 100644 --- a/src/com/android/tv/data/epg/EpgFetcherImpl.java +++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java @@ -38,11 +38,10 @@ import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.config.api.RemoteConfigValue; +import com.android.tv.common.buildtype.HasBuildType; import com.android.tv.common.util.Clock; import com.android.tv.common.util.CommonUtils; import com.android.tv.common.util.LocationUtils; @@ -55,12 +54,15 @@ import com.android.tv.data.ChannelLogoFetcher; import com.android.tv.data.Lineup; import com.android.tv.data.Program; import com.android.tv.data.api.Channel; +import com.android.tv.features.TvFeatures; import com.android.tv.perf.EventNames; import com.android.tv.perf.PerformanceMonitor; import com.android.tv.perf.TimerEvent; import com.android.tv.util.Utils; import com.google.android.tv.partner.support.EpgInput; import com.google.android.tv.partner.support.EpgInputs; +import com.google.common.collect.ImmutableSet; +import com.android.tv.common.flags.BackendKnobsFlags; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -100,8 +102,7 @@ public class EpgFetcherImpl implements EpgFetcher { private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2); - private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR = - RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4); + private static final long DEFAULT_ROUTINE_INTERVAL_HOUR = 4; private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1; private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2; @@ -115,6 +116,9 @@ public class EpgFetcherImpl implements EpgFetcher { private final ChannelDataManager mChannelDataManager; private final EpgReader mEpgReader; private final PerformanceMonitor mPerformanceMonitor; + private final EpgInputWhiteList mEpgInputWhiteList; + private final BackendKnobsFlags mBackendKnobsFlags; + private final HasBuildType.BuildType mBuildType; private FetchAsyncTask mFetchTask; private FetchDuringScanHandler mFetchDuringScanHandler; private long mEpgTimeStamp; @@ -124,9 +128,6 @@ public class EpgFetcherImpl implements EpgFetcher { // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished. private boolean mScanStarted; - private final long mRoutineIntervalMs; - private final long mEpgDataExpiredTimeLimitMs; - private final long mFastFetchDurationSec; private Clock mClock; public static EpgFetcher create(Context context) { @@ -136,36 +137,54 @@ public class EpgFetcherImpl implements EpgFetcher { PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor(); EpgReader epgReader = tvSingletons.providesEpgReader().get(); Clock clock = tvSingletons.getClock(); - long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig()); - + EpgInputWhiteList epgInputWhiteList = + new EpgInputWhiteList(tvSingletons.getCloudEpgFlags()); + BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs(); + HasBuildType.BuildType buildType = tvSingletons.getBuildType(); return new EpgFetcherImpl( context, + epgInputWhiteList, channelDataManager, epgReader, performanceMonitor, clock, - routineIntervalMs); + backendKnobsFlags, + buildType); } @VisibleForTesting EpgFetcherImpl( Context context, + EpgInputWhiteList epgInputWhiteList, ChannelDataManager channelDataManager, EpgReader epgReader, PerformanceMonitor performanceMonitor, Clock clock, - long routineIntervalMs) { + BackendKnobsFlags backendKnobsFlags, + HasBuildType.BuildType buildType) { mContext = context; mChannelDataManager = channelDataManager; mEpgReader = epgReader; mPerformanceMonitor = performanceMonitor; mClock = clock; - mRoutineIntervalMs = - routineIntervalMs <= 0 - ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue()) - : TimeUnit.HOURS.toMillis(routineIntervalMs); - mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2; - mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000; + mEpgInputWhiteList = epgInputWhiteList; + mBackendKnobsFlags = backendKnobsFlags; + mBuildType = buildType; + } + + private long getFastFetchDurationSec() { + return FAST_FETCH_DURATION_SEC + getRoutineIntervalMs() / 1000; + } + + private long getEpgDataExpiredTimeLimitMs() { + return getRoutineIntervalMs() * 2; + } + + private long getRoutineIntervalMs() { + long routineIntervalHours = mBackendKnobsFlags.epgFetcherIntervalHour(); + return routineIntervalHours <= 0 + ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR) + : TimeUnit.HOURS.toMillis(routineIntervalHours); } private static Set<Channel> getExistingChannelsForMyPackage(Context context) { @@ -214,7 +233,7 @@ public class EpgFetcherImpl implements EpgFetcher { new JobInfo.Builder( EPG_ROUTINELY_FETCHING_JOB_ID, new ComponentName(mContext, EpgFetchService.class)) - .setPeriodic(mRoutineIntervalMs) + .setPeriodic(getRoutineIntervalMs()) .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL) .setPersisted(true) .build(); @@ -238,7 +257,7 @@ public class EpgFetcherImpl implements EpgFetcher { @Override protected void onPostExecute(Long result) { if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { + > getEpgDataExpiredTimeLimitMs()) { Log.i(TAG, "EPG data expired. Start fetching immediately."); fetchImmediately(); } @@ -346,6 +365,19 @@ public class EpgFetcherImpl implements EpgFetcher { if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels."); return false; } + if (!TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext) + && mBuildType != HasBuildType.BuildType.AOSP) { + if (getTunerChannelCount() == 0) { + if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels."); + return false; + } + if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) { + return true; + } + if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return true; + } + } return true; } @@ -505,6 +537,17 @@ public class EpgFetcherImpl implements EpgFetcher { return numbers.size(); } + private boolean isInputInWhiteList(EpgInput epgInput) { + if (mBuildType == HasBuildType.BuildType.AOSP) { + return false; + } + return (BuildConfig.ENG + && epgInput.getInputId() + .equals( + "com.example.partnersupportsampletvinput/.SampleTvInputService")) + || mEpgInputWhiteList.isInputWhiteListed(epgInput.getInputId()); + } + @VisibleForTesting class FetchAsyncTask extends AsyncTask<Void, Void, Integer> { private final JobService mService; @@ -532,12 +575,45 @@ public class EpgFetcherImpl implements EpgFetcher { Integer builtInResult = fetchEpgForBuiltInTuner(); boolean anyCloudEpgFailure = false; boolean anyCloudEpgSuccess = false; + if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext) + && mBuildType != HasBuildType.BuildType.AOSP) { + for (EpgInput epgInput : getEpgInputs()) { + if (DEBUG) Log.d(TAG, "Start EPG fetch for " + epgInput); + if (isCancelled()) { + break; + } + if (isInputInWhiteList(epgInput)) { + // TODO(b/66191312) check timestamp and result code and decide if update + // is needed. + Set<Channel> channels = getExistingChannelsFor(epgInput.getInputId()); + Integer result = fetchEpgFor(epgInput.getLineupId(), channels); + anyCloudEpgFailure = anyCloudEpgFailure || result != null; + anyCloudEpgSuccess = anyCloudEpgSuccess || result == null; + updateCloudEpgInput(epgInput, result); + } else { + Log.w( + TAG, + "Fetching the EPG for " + + epgInput.getInputId() + + " is not supported."); + } + } + } + if (builtInResult == null || builtInResult == REASON_NO_BUILT_IN_CHANNELS) { + return anyCloudEpgFailure + ? ((Integer) REASON_CLOUD_EPG_FAILURE) + : anyCloudEpgSuccess ? null : builtInResult; + } return builtInResult; } finally { TrafficStats.setThreadStatsTag(oldTag); } } + private void updateCloudEpgInput(EpgInput unusedEpgInput, Integer unusedResult) { + // TODO(b/66191312) write the result and timestamp to the input table + } + private Set<Channel> getExistingChannelsFor(String inputId) { Set<Channel> result = new HashSet<>(); try (Cursor cursor = @@ -548,13 +624,24 @@ public class EpgFetcherImpl implements EpgFetcher { null, null, null)) { - while (cursor.moveToNext()) { - result.add(ChannelImpl.fromCursor(cursor)); + if (cursor != null) { + while (cursor.moveToNext()) { + result.add(ChannelImpl.fromCursor(cursor)); + } } return result; } } + private Set<EpgInput> getEpgInputs() { + if (mBuildType == HasBuildType.BuildType.AOSP) { + return ImmutableSet.of(); + } + Set<EpgInput> epgInputs = EpgInputs.queryEpgInputs(mContext.getContentResolver()); + if (DEBUG) Log.d(TAG, "getEpgInputs " + epgInputs); + return epgInputs; + } + private Integer fetchEpgForBuiltInTuner() { try { Integer failureReason = prepareFetchEpg(false); @@ -606,19 +693,16 @@ public class EpgFetcherImpl implements EpgFetcher { Log.i(TAG, "Failed to get EPG channels for " + lineupId); return REASON_NO_EPG_DATA_RETURNED; } + EpgFetchHelper.updateNetworkAffiliation(mContext, channels); if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { - batchFetchEpg(channels, mFastFetchDurationSec); + > getEpgDataExpiredTimeLimitMs()) { + batchFetchEpg(channels, getFastFetchDurationSec()); } new Handler(mContext.getMainLooper()) .post( - new Runnable() { - @Override - public void run() { + () -> ChannelLogoFetcher.startFetchingChannelLogos( - mContext, asChannelList(channels)); - } - }); + mContext, asChannelList(channels))); for (EpgReader.EpgChannel epgChannel : channels) { if (this.isCancelled()) { return null; @@ -780,6 +864,9 @@ public class EpgFetcherImpl implements EpgFetcher { mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId()); } } + if (!newChannels.isEmpty()) { + EpgFetchHelper.updateNetworkAffiliation(mContext, newChannels); + } batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC); } @@ -798,14 +885,7 @@ public class EpgFetcherImpl implements EpgFetcher { // Clear timestamp to make routine service start right away. EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0); Log.i(TAG, "EPG Fetching during channel scanning finished."); - new Handler(Looper.getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - fetchImmediately(); - } - }); + new Handler(Looper.getMainLooper()).post(EpgFetcherImpl.this::fetchImmediately); } } } diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java index eada8b24..24b4fe3d 100644 --- a/src/com/android/tv/data/epg/EpgInputWhiteList.java +++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java @@ -21,8 +21,8 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import com.android.tv.common.BuildConfig; -import com.android.tv.common.config.api.RemoteConfig; import com.android.tv.common.experiments.Experiments; +import com.android.tv.common.flags.CloudEpgFlags; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -33,7 +33,6 @@ import java.util.Set; public final class EpgInputWhiteList { private static final boolean DEBUG = false; private static final String TAG = "EpgInputWhiteList"; - @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs"; private static final String QA_DEV_INPUTS = "com.example.partnersupportsampletvinput/.SampleTvInputService," + "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService"; @@ -44,10 +43,10 @@ public final class EpgInputWhiteList { return inputId == null ? null : inputId.substring(0, inputId.indexOf("/")); } - private final RemoteConfig remoteConfig; + private final CloudEpgFlags cloudEpgFlags; - public EpgInputWhiteList(RemoteConfig remoteConfig) { - this.remoteConfig = remoteConfig; + public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags) { + this.cloudEpgFlags = cloudEpgFlags; } public boolean isInputWhiteListed(String inputId) { @@ -72,7 +71,7 @@ public final class EpgInputWhiteList { } private Set<String> getWhiteListedInputs() { - Set<String> result = toInputSet(remoteConfig.getString(KEY)); + Set<String> result = toInputSet(cloudEpgFlags.thirdPartyEpgInputsCsv()); if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) { HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS)); if (result.isEmpty()) { diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index 7147905a..c9fcd979 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -23,6 +23,7 @@ import com.android.tv.data.Lineup; import com.android.tv.data.Program; import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.SeriesInfo; +import com.google.auto.value.AutoValue; import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,15 +34,18 @@ import java.util.Set; public interface EpgReader { /** Value class that holds a EpgChannelId and its corresponding {@link Channel} */ - // TODO(b/72052568): Get autovalue to work in aosp master + @AutoValue abstract class EpgChannel { - public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) { - return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId); + public static EpgChannel createEpgChannel(Channel channel, String epgChannelId, + boolean dbUpdateNeeded) { + return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId, dbUpdateNeeded); } public abstract Channel getChannel(); public abstract String getEpgChannelId(); + + public abstract boolean getDbUpdateNeeded(); } /** Checks if the reader is available. */ diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java index 71f45fbe..87308093 100644 --- a/src/com/android/tv/dialog/PinDialogFragment.java +++ b/src/com/android/tv/dialog/PinDialogFragment.java @@ -16,37 +16,26 @@ package com.android.tv.dialog; -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.app.ActivityManager; import android.app.Dialog; -import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; -import android.content.res.Resources; import android.media.tv.TvContentRating; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.text.TextUtils; -import android.util.AttributeSet; import android.util.Log; -import android.util.TypedValue; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; -import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.dialog.picker.PinPicker; import com.android.tv.util.TvSettings; public class PinDialogFragment extends SafeDismissDialogFragment { @@ -77,17 +66,12 @@ public class PinDialogFragment extends SafeDismissDialogFragment { private static final int MAX_WRONG_PIN_COUNT = 5; private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute - private static final String INITIAL_TEXT = "—"; private static final String TRACKER_LABEL = "Pin dialog"; private static final String ARGS_TYPE = "args_type"; private static final String ARGS_RATING = "args_rating"; public static final String DIALOG_TAG = PinDialogFragment.class.getName(); - private static final int NUMBER_PICKERS_RES_ID[] = { - R.id.first, R.id.second, R.id.third, R.id.fourth - }; - private int mType; private int mRequestType; private boolean mPinChecked; @@ -96,7 +80,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment { private TextView mWrongPinView; private View mEnterPinView; private TextView mTitleView; - private PinNumberPicker[] mPickers; + private PinPicker mPicker; private SharedPreferences mSharedPreferences; private String mPrevPin; private String mPin; @@ -140,7 +124,6 @@ public class PinDialogFragment extends SafeDismissDialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dlg = super.onCreateDialog(savedInstanceState); dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; - PinNumberPicker.loadResources(dlg.getContext()); return dlg; } @@ -171,6 +154,14 @@ public class PinDialogFragment extends SafeDismissDialogFragment { mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin); mEnterPinView = v.findViewById(R.id.enter_pin); mTitleView = (TextView) mEnterPinView.findViewById(R.id.title); + mPicker = v.findViewById(R.id.pin_picker); + mPicker.setOnClickListener( + view -> { + String pin = getPinInput(); + if (!TextUtils.isEmpty(pin)) { + done(pin); + } + }); if (TextUtils.isEmpty(getPin())) { // If PIN isn't set, user should set a PIN. // Successfully setting a new set is considered as entering correct PIN. @@ -210,31 +201,13 @@ public class PinDialogFragment extends SafeDismissDialogFragment { } } - mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length]; - for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) { - mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]); - mPickers[i].setValueRangeAndResetText(0, 9); - mPickers[i].setPinDialogFragment(this); - mPickers[i].updateFocus(false); - } - for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) { - mPickers[i].setNextNumberPicker(mPickers[i + 1]); - } - if (mType != PIN_DIALOG_TYPE_NEW_PIN) { updateWrongPin(); } + mPicker.requestFocus(); return v; } - private final Runnable mUpdateEnterPinRunnable = - new Runnable() { - @Override - public void run() { - updateWrongPin(); - } - }; - private void updateWrongPin() { if (getActivity() == null) { // The activity is already detached. No need to update. @@ -257,7 +230,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { R.plurals.pin_enter_countdown, remainingSeconds, remainingSeconds)); - mHandler.postDelayed(mUpdateEnterPinRunnable, 1000); + + mHandler.postDelayed(this::updateWrongPin, 1000); } } @@ -364,383 +338,11 @@ public class PinDialogFragment extends SafeDismissDialogFragment { } private String getPinInput() { - String result = ""; - try { - for (PinNumberPicker pnp : mPickers) { - pnp.updateText(); - result += pnp.getValue(); - } - } catch (IllegalStateException e) { - result = ""; - } - return result; + return mPicker.getPinInput(); } private void resetPinInput() { - for (PinNumberPicker pnp : mPickers) { - pnp.setValueRangeAndResetText(0, 9); - } - mPickers[0].requestFocus(); - } - - public static class PinNumberPicker extends FrameLayout { - private static final int NUMBER_VIEWS_RES_ID[] = { - R.id.previous2_number, - R.id.previous_number, - R.id.current_number, - R.id.next_number, - R.id.next2_number - }; - private static final int CURRENT_NUMBER_VIEW_INDEX = 2; - private static final int NOT_INITIALIZED = Integer.MIN_VALUE; - - private static Animator sFocusedNumberEnterAnimator; - private static Animator sFocusedNumberExitAnimator; - private static Animator sAdjacentNumberEnterAnimator; - private static Animator sAdjacentNumberExitAnimator; - - private static float sAlphaForFocusedNumber; - private static float sAlphaForAdjacentNumber; - - private int mMinValue; - private int mMaxValue; - private int mCurrentValue; - // a value for setting mCurrentValue at the end of scroll animation. - private int mNextValue; - private final int mNumberViewHeight; - private PinDialogFragment mDialog; - private PinNumberPicker mNextNumberPicker; - private boolean mCancelAnimation; - - private final View mNumberViewHolder; - // When the PinNumberPicker has focus, mBackgroundView will show the focused background. - // Also, this view is used for handling the text change animation of the current number - // view which is required when the current number view text is changing from INITIAL_TEXT - // to "0". - private final TextView mBackgroundView; - private final TextView[] mNumberViews; - private final AnimatorSet mFocusGainAnimator; - private final AnimatorSet mFocusLossAnimator; - private final AnimatorSet mScrollAnimatorSet; - - public PinNumberPicker(Context context) { - this(context, null); - } - - public PinNumberPicker(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public PinNumberPicker( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - View view = inflate(context, R.layout.pin_number_picker, this); - mNumberViewHolder = view.findViewById(R.id.number_view_holder); - mBackgroundView = (TextView) view.findViewById(R.id.focused_background); - mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length]; - for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { - mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]); - } - Resources resources = context.getResources(); - mNumberViewHeight = - resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height); - - mNumberViewHolder.setOnFocusChangeListener( - new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - updateFocus(true); - } - }); - - mNumberViewHolder.setOnKeyListener( - new OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - { - if (mCancelAnimation) { - mScrollAnimatorSet.end(); - } - if (!mScrollAnimatorSet.isRunning()) { - mCancelAnimation = false; - if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mNextValue = - adjustValueInValidRange( - mCurrentValue + 1); - startScrollAnimation(true); - } else { - mNextValue = - adjustValueInValidRange( - mCurrentValue - 1); - startScrollAnimation(false); - } - } - return true; - } - } - } else if (event.getAction() == KeyEvent.ACTION_UP) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - { - mCancelAnimation = true; - return true; - } - } - } - return false; - } - }); - mNumberViewHolder.setScrollY(mNumberViewHeight); - - mFocusGainAnimator = new AnimatorSet(); - mFocusGainAnimator.playTogether( - ObjectAnimator.ofFloat( - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], - "alpha", - 0f, - sAlphaForAdjacentNumber), - ObjectAnimator.ofFloat( - mNumberViews[CURRENT_NUMBER_VIEW_INDEX], - "alpha", - sAlphaForFocusedNumber, - 0f), - ObjectAnimator.ofFloat( - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], - "alpha", - 0f, - sAlphaForAdjacentNumber), - ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f)); - mFocusGainAnimator.setDuration( - context.getResources().getInteger(android.R.integer.config_shortAnimTime)); - mFocusGainAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText( - mBackgroundView.getText()); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( - sAlphaForFocusedNumber); - mBackgroundView.setText(""); - } - }); - - mFocusLossAnimator = new AnimatorSet(); - mFocusLossAnimator.playTogether( - ObjectAnimator.ofFloat( - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], - "alpha", - sAlphaForAdjacentNumber, - 0f), - ObjectAnimator.ofFloat( - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], - "alpha", - sAlphaForAdjacentNumber, - 0f), - ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f)); - mFocusLossAnimator.setDuration( - context.getResources().getInteger(android.R.integer.config_shortAnimTime)); - - mScrollAnimatorSet = new AnimatorSet(); - mScrollAnimatorSet.setDuration( - context.getResources().getInteger(R.integer.pin_number_scroll_duration)); - mScrollAnimatorSet.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Set mCurrent value when scroll animation is finished. - mCurrentValue = mNextValue; - updateText(); - mNumberViewHolder.setScrollY(mNumberViewHeight); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha( - sAlphaForAdjacentNumber); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( - sAlphaForFocusedNumber); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha( - sAlphaForAdjacentNumber); - } - }); - } - - static void loadResources(Context context) { - if (sFocusedNumberEnterAnimator == null) { - TypedValue outValue = new TypedValue(); - context.getResources() - .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true); - sAlphaForFocusedNumber = outValue.getFloat(); - context.getResources() - .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true); - sAlphaForAdjacentNumber = outValue.getFloat(); - - sFocusedNumberEnterAnimator = - AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter); - sFocusedNumberExitAnimator = - AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit); - sAdjacentNumberEnterAnimator = - AnimatorInflater.loadAnimator( - context, R.animator.pin_adjacent_number_enter); - sAdjacentNumberExitAnimator = - AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP) { - int keyCode = event.getKeyCode(); - if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { - mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0); - updateFocus(false); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { - if (mNextNumberPicker == null) { - String pin = mDialog.getPinInput(); - if (!TextUtils.isEmpty(pin)) { - mDialog.done(pin); - } - } else { - mNextNumberPicker.requestFocus(); - } - return true; - } - } - return super.dispatchKeyEvent(event); - } - - void startScrollAnimation(boolean scrollUp) { - mFocusGainAnimator.end(); - mFocusLossAnimator.end(); - final ValueAnimator scrollAnimator = - ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight); - scrollAnimator.addUpdateListener( - new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - mNumberViewHolder.setScrollY(value + mNumberViewHeight); - } - }); - scrollAnimator.setDuration( - getResources().getInteger(R.integer.pin_number_scroll_duration)); - - if (scrollUp) { - sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); - sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); - sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); - sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]); - } else { - sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]); - sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); - sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); - sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); - } - - mScrollAnimatorSet.playTogether( - scrollAnimator, - sAdjacentNumberExitAnimator, - sFocusedNumberExitAnimator, - sFocusedNumberEnterAnimator, - sAdjacentNumberEnterAnimator); - mScrollAnimatorSet.start(); - } - - void setValueRangeAndResetText(int min, int max) { - if (min > max) { - throw new IllegalArgumentException( - "The min value should be greater than or equal to the max value"); - } else if (min == NOT_INITIALIZED) { - throw new IllegalArgumentException( - "The min value should be greater than Integer.MIN_VALUE."); - } - mMinValue = min; - mMaxValue = max; - mNextValue = mCurrentValue = NOT_INITIALIZED; - for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { - mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : ""); - } - mBackgroundView.setText(INITIAL_TEXT); - } - - void setPinDialogFragment(PinDialogFragment dlg) { - mDialog = dlg; - } - - void setNextNumberPicker(PinNumberPicker picker) { - mNextNumberPicker = picker; - } - - int getValue() { - if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) { - throw new IllegalStateException("Value is not set"); - } - return mCurrentValue; - } - - void updateFocus(boolean withAnimation) { - mScrollAnimatorSet.end(); - mFocusGainAnimator.end(); - mFocusLossAnimator.end(); - updateText(); - if (mNumberViewHolder.isFocused()) { - if (withAnimation) { - mBackgroundView.setText(String.valueOf(mCurrentValue)); - mFocusGainAnimator.start(); - } else { - mBackgroundView.setAlpha(1f); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber); - } - } else { - if (withAnimation) { - mFocusLossAnimator.start(); - } else { - mBackgroundView.setAlpha(0f); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f); - } - mNumberViewHolder.setScrollY(mNumberViewHeight); - } - } - - private void updateText() { - boolean wasNotInitialized = false; - if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) { - mNextValue = mCurrentValue = mMinValue; - wasNotInitialized = true; - } - if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) { - for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { - if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) { - // In order to show the text change animation, keep the text of - // mNumberViews[CURRENT_NUMBER_VIEW_INDEX]. - } else { - mNumberViews[i].setText( - String.valueOf( - adjustValueInValidRange( - mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i))); - } - } - } - } - - private int adjustValueInValidRange(int value) { - int interval = mMaxValue - mMinValue + 1; - if (value < mMinValue - interval || value > mMaxValue + interval) { - throw new IllegalArgumentException( - "The value( " + value + ") is too small or too big to adjust"); - } - return (value < mMinValue) - ? value + interval - : (value > mMaxValue) ? value - interval : value; - } + mPicker.resetPinInput(); } /** diff --git a/src/com/android/tv/dialog/picker/PinPicker.java b/src/com/android/tv/dialog/picker/PinPicker.java new file mode 100644 index 00000000..f501dfd1 --- /dev/null +++ b/src/com/android/tv/dialog/picker/PinPicker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 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.picker; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v17.leanback.widget.picker.Picker; +import android.support.v17.leanback.widget.picker.PickerColumn; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import java.util.ArrayList; +import java.util.List; + +/** 4 digit picker */ +public final class PinPicker extends Picker { + // TODO(b/116144491): use leanback pin picker. + + private final List<PickerColumn> mPickers = new ArrayList<>(); + private OnClickListener mOnClickListener; + + // the version of picker I link to does not have this constructor + public PinPicker(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public PinPicker(Context context, AttributeSet attributeSet, int defStyleAttr) { + super(context, attributeSet, defStyleAttr); + + for (int i = 0; i < 4; i++) { + PickerColumn pickerColumn = new PickerColumn(); + pickerColumn.setMinValue(0); + pickerColumn.setMaxValue(9); + pickerColumn.setLabelFormat("%d"); + mPickers.add(pickerColumn); + } + setSeparator(" "); + setColumns(mPickers); + setActivated(true); + setFocusable(true); + super.setOnClickListener(this::onClick); + } + + public String getPinInput() { + String result = ""; + try { + for (PickerColumn column : mPickers) { + + result += column.getCurrentValue(); + } + } catch (IllegalStateException e) { + result = ""; + } + return result; + } + + @Override + public void setOnClickListener(@Nullable OnClickListener l) { + mOnClickListener = l; + } + + private void onClick(View v) { + int selectedColumn = getSelectedColumn(); + int nextColumn = selectedColumn + 1; + // Only call the click listener if we are on the last column + // Otherwise move to the next column + if (nextColumn == getColumnsCount()) { + if (mOnClickListener != null) { + mOnClickListener.onClick(v); + } + } else { + setSelectedColumn(nextColumn); + onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null); + } + } + + public void resetPinInput() { + setActivated(false); + for (int i = 0; i < 4; i++) { + setColumnValue(i, 0, true); + } + setSelectedColumn(0); + setActivated(true); // This resets the focus + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + int keyCode = event.getKeyCode(); + int digit = digitFromKeyCode(keyCode); + if (digit != -1) { + int selectedColumn = getSelectedColumn(); + setColumnValue(selectedColumn, digit, false); + int nextColumn = selectedColumn + 1; + if (nextColumn < getColumnsCount()) { + setSelectedColumn(nextColumn); + onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null); + } else { + callOnClick(); + } + return true; + } + } + return super.dispatchKeyEvent(event); + } + + @VisibleForTesting + static int digitFromKeyCode(int keyCode) { + if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + return keyCode - KeyEvent.KEYCODE_0; + } else if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) { + return keyCode - KeyEvent.KEYCODE_NUMPAD_0; + } + return -1; + } +} diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 2b4ecbf5..0053650b 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -16,7 +16,6 @@ package com.android.tv.dvr; -import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentUris; @@ -49,21 +48,23 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.RecordingState; import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; -import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; +import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture; +import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture; +import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture; +import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture; +import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture; +import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture; +import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture; +import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture; import com.android.tv.dvr.provider.DvrDbSync; import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; -import com.android.tv.util.Filter; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvUriMatcher; +import com.google.common.base.Predicate; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -73,6 +74,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.Future; /** DVR Data manager to handle recordings and schedules. */ @MainThread @@ -106,8 +108,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Override public void onChange(boolean selfChange, final @Nullable Uri uri) { - RecordedProgramsQueryTask task = - new RecordedProgramsQueryTask(mContext.getContentResolver(), uri); + RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri); task.executeOnDbThread(); mPendingTasks.add(task); } @@ -116,6 +117,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private boolean mDvrLoadFinished; private boolean mRecordedProgramLoadFinished; private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); + private final Set<Future> mPendingDvrFuture = new ArraySet<>(); + // TODO(b/79207567) make sure Future is not stopped at writing. + private final Set<Future> mNoStopFuture = new ArraySet<>(); private DvrDbSync mDbSync; private RecordingStorageStatusManager mStorageStatusManager; @@ -154,13 +158,27 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } }; + private final FutureCallback<Void> removeFromSetOnCompletion = + new FutureCallback<Void>() { + @Override + public void onSuccess(Void result) { + mNoStopFuture.remove(this); + } + + @Override + public void onFailure(Throwable t) { + Log.w(TAG, "Failed to execute.", t); + mNoStopFuture.remove(this); + } + }; + private static <T> List<T> moveElements( - HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) { + HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) { List<T> moved = new ArrayList<>(); Iterator<Entry<Long, T>> iter = from.entrySet().iterator(); while (iter.hasNext()) { Entry<Long, T> entry = iter.next(); - if (filter.filter(entry.getValue())) { + if (filter.apply(entry.getValue())) { to.put(entry.getKey(), entry.getValue()); iter.remove(); moved.add(entry.getValue()); @@ -181,134 +199,143 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void start() { mInputManager.addCallback(mInputCallback); mStorageStatusManager.addListener(mStorageMountChangedListener); - AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask = - new AsyncDvrQuerySeriesRecordingTask(mContext) { - @Override - protected void onCancelled(List<SeriesRecording> seriesRecordings) { - mPendingTasks.remove(this); - } - - @Override - protected void onPostExecute(List<SeriesRecording> seriesRecordings) { - mPendingTasks.remove(this); - long maxId = 0; - HashSet<String> seriesIds = new HashSet<>(); - for (SeriesRecording r : seriesRecordings) { - if (SoftPreconditions.checkState( - !seriesIds.contains(r.getSeriesId()), - TAG, - "Skip loading series recording with duplicate series ID: " - + r)) { - seriesIds.add(r.getSeriesId()); - if (isInputAvailable(r.getInputId())) { - mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); - } else { - mSeriesRecordingsForRemovedInput.put(r.getId(), r); + DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask = + new DvrQuerySeriesRecordingFuture(mContext); + ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture = + dvrQuerySeriesRecordingTask.executeOnDbThread( + new FutureCallback<List<SeriesRecording>>() { + @Override + public void onSuccess(List<SeriesRecording> seriesRecordings) { + mPendingDvrFuture.remove(this); + long maxId = 0; + HashSet<String> seriesIds = new HashSet<>(); + for (SeriesRecording r : seriesRecordings) { + if (SoftPreconditions.checkState( + !seriesIds.contains(r.getSeriesId()), + TAG, + "Skip loading series recording with duplicate series ID: " + + r)) { + seriesIds.add(r.getSeriesId()); + if (isInputAvailable(r.getInputId())) { + mSeriesRecordings.put(r.getId(), r); + mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + } else { + mSeriesRecordingsForRemovedInput.put(r.getId(), r); + } + } + if (maxId < r.getId()) { + maxId = r.getId(); + } } + IdGenerator.SERIES_RECORDING.setMaxId(maxId); } - if (maxId < r.getId()) { - maxId = r.getId(); - } - } - IdGenerator.SERIES_RECORDING.setMaxId(maxId); - } - }; - dvrQuerySeriesRecordingTask.executeOnDbThread(); - mPendingTasks.add(dvrQuerySeriesRecordingTask); - AsyncDvrQueryScheduleTask dvrQueryScheduleTask = - new AsyncDvrQueryScheduleTask(mContext) { - @Override - protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { - mPendingTasks.remove(this); - } - @SuppressLint("SwitchIntDef") - @Override - protected void onPostExecute(List<ScheduledRecording> result) { - mPendingTasks.remove(this); - long maxId = 0; - int reasonNotStarted = - ScheduledRecording - .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; - List<ScheduledRecording> toUpdate = new ArrayList<>(); - List<ScheduledRecording> toDelete = new ArrayList<>(); - for (ScheduledRecording r : result) { - if (!isInputAvailable(r.getInputId())) { - mScheduledRecordingsForRemovedInput.put(r.getId(), r); - } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { - getDeletedScheduleMap().put(r.getProgramId(), r); - } else { - mScheduledRecordings.put(r.getId(), r); - if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { - mProgramId2ScheduledRecordings.put(r.getProgramId(), r); - } - // Adjust the state of the schedules before DB loading is finished. - switch (r.getState()) { - case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - int reason = - ScheduledRecording.FAILED_REASON_NOT_FINISHED; - toUpdate.add( - ScheduledRecording.buildFrom(r) - .setState( - ScheduledRecording - .STATE_RECORDING_FAILED) - .setFailedReason(reason) - .build()); - } else { - toUpdate.add( - ScheduledRecording.buildFrom(r) - .setState( - ScheduledRecording - .STATE_RECORDING_NOT_STARTED) - .build()); + @Override + public void onFailure(Throwable t) { + Log.w(TAG, "Failed to load series recording.", t); + mPendingDvrFuture.remove(this); + } + }); + mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture); + DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext); + ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture = + dvrQueryScheduleTask.executeOnDbThread( + new FutureCallback<List<ScheduledRecording>>() { + @Override + public void onSuccess(List<ScheduledRecording> result) { + mPendingDvrFuture.remove(this); + long maxId = 0; + int reasonNotStarted = + ScheduledRecording + .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; + List<ScheduledRecording> toUpdate = new ArrayList<>(); + List<ScheduledRecording> toDelete = new ArrayList<>(); + for (ScheduledRecording r : result) { + if (!isInputAvailable(r.getInputId())) { + mScheduledRecordingsForRemovedInput.put(r.getId(), r); + } else if (r.getState() + == ScheduledRecording.STATE_RECORDING_DELETED) { + getDeletedScheduleMap().put(r.getProgramId(), r); + } else { + mScheduledRecordings.put(r.getId(), r); + if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { + mProgramId2ScheduledRecordings.put(r.getProgramId(), r); } - break; - case ScheduledRecording.STATE_RECORDING_NOT_STARTED: - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add( - ScheduledRecording.buildFrom(r) - .setState( - ScheduledRecording - .STATE_RECORDING_FAILED) - .setFailedReason(reasonNotStarted) - .build()); + // Adjust the state of the schedules before DB loading is + // finished. + switch (r.getState()) { + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + if (r.getEndTimeMs() + <= mClock.currentTimeMillis()) { + int reason = + ScheduledRecording + .FAILED_REASON_NOT_FINISHED; + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_FAILED) + .setFailedReason(reason) + .build()); + } else { + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_NOT_STARTED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + if (r.getEndTimeMs() + <= mClock.currentTimeMillis()) { + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_FAILED) + .setFailedReason( + reasonNotStarted) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_CANCELED: + toDelete.add(r); + break; + default: // fall out } - break; - case ScheduledRecording.STATE_RECORDING_CANCELED: - toDelete.add(r); - break; - default: // fall out + } + if (maxId < r.getId()) { + maxId = r.getId(); + } + } + if (!toUpdate.isEmpty()) { + updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); + } + if (!toDelete.isEmpty()) { + removeScheduledRecording(ScheduledRecording.toArray(toDelete)); + } + IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); + if (mRecordedProgramLoadFinished) { + validateSeriesRecordings(); + } + mDvrLoadFinished = true; + notifyDvrScheduleLoadFinished(); + if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); + SeriesRecordingScheduler.getInstance(mContext).start(); } } - if (maxId < r.getId()) { - maxId = r.getId(); + + @Override + public void onFailure(Throwable t) { + Log.w(TAG, "Failed to load scheduled recording.", t); + mPendingDvrFuture.remove(this); } - } - if (!toUpdate.isEmpty()) { - updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); - } - if (!toDelete.isEmpty()) { - removeScheduledRecording(ScheduledRecording.toArray(toDelete)); - } - IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); - if (mRecordedProgramLoadFinished) { - validateSeriesRecordings(); - } - mDvrLoadFinished = true; - notifyDvrScheduleLoadFinished(); - if (isInitialized()) { - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); - SeriesRecordingScheduler.getInstance(mContext).start(); - } - } - }; - dvrQueryScheduleTask.executeOnDbThread(); - mPendingTasks.add(dvrQueryScheduleTask); - RecordedProgramsQueryTask mRecordedProgramQueryTask = - new RecordedProgramsQueryTask(mContext.getContentResolver(), null); + }); + mPendingDvrFuture.add(dvrQueryScheduleFuture); + RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null); mRecordedProgramQueryTask.executeOnDbThread(); ContentResolver cr = mContext.getContentResolver(); cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver); @@ -329,6 +356,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { i.remove(); task.cancel(true); } + Iterator<Future> id = mPendingDvrFuture.iterator(); + while (id.hasNext()) { + Future future = id.next(); + id.remove(); + future.cancel(true); + } } private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) { @@ -607,7 +640,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (mDvrLoadFinished) { notifyScheduledRecordingAdded(schedules); } - new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules); + ListenableFuture addScheduleFuture = + new AddScheduleFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, schedules); + mNoStopFuture.add(addScheduleFuture); removeDeletedSchedules(schedules); } @@ -626,7 +662,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (mDvrLoadFinished) { notifySeriesRecordingAdded(seriesRecordings); } - new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings); + ListenableFuture addSeriesRecordingFuture = + new AddSeriesRecordingFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); + mNoStopFuture.add(addSeriesRecordingFuture); } @Override @@ -683,12 +722,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext) - .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); + ListenableFuture deleteScheduleFuture = + new DeleteScheduleFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + ScheduledRecording.toArray(schedulesToDelete)); + mNoStopFuture.add(deleteScheduleFuture); } if (!schedulesNotToDelete.isEmpty()) { - new AsyncUpdateScheduleTask(mContext) - .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete)); + ListenableFuture updateScheduleFuture = + new UpdateScheduleFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + ScheduledRecording.toArray(schedulesNotToDelete)); + mNoStopFuture.add(updateScheduleFuture); } } @@ -726,7 +773,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (mDvrLoadFinished) { notifySeriesRecordingRemoved(seriesRecordings); } - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings); + ListenableFuture deleteSeriesRecordingFuture = + new DeleteSeriesRecordingFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); + mNoStopFuture.add(deleteSeriesRecordingFuture); removeDeletedSchedules(seriesRecordings); } @@ -778,7 +828,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { notifyScheduledRecordingStatusChanged(scheduleArray); } if (updateDb) { - new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); + ListenableFuture updateScheduleFuture = + new UpdateScheduleFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, scheduleArray); + mNoStopFuture.add(updateScheduleFuture); } checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); removeDeletedSchedules(schedules); @@ -802,7 +855,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (mDvrLoadFinished) { notifySeriesRecordingChanged(seriesRecordings); } - new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings); + ListenableFuture updateSeriesRecordingFuture = + new UpdateSeriesRecordingFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings); + mNoStopFuture.add(updateSeriesRecordingFuture); } private boolean isInputAvailable(String inputId) { @@ -820,8 +876,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext) - .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); + ListenableFuture deleteScheduleFuture = + new DeleteScheduleFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + ScheduledRecording.toArray(schedulesToDelete)); + mNoStopFuture.add(deleteScheduleFuture); } } @@ -841,8 +901,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext) - .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); + ListenableFuture deleteScheduleFuture = + new DeleteScheduleFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + ScheduledRecording.toArray(schedulesToDelete)); + mNoStopFuture.add(deleteScheduleFuture); } } @@ -852,38 +916,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { moveElements( mScheduledRecordingsForRemovedInput, mScheduledRecordings, - new Filter<ScheduledRecording>() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + r -> r.getInputId().equals(inputId)); List<RecordedProgram> movedRecordedPrograms = moveElements( mRecordedProgramsForRemovedInput, mRecordedPrograms, - new Filter<RecordedProgram>() { - @Override - public boolean filter(RecordedProgram r) { - return r.getInputId().equals(inputId); - } - }); + r -> r.getInputId().equals(inputId)); List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); List<SeriesRecording> movedSeriesRecordings = moveElements( mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - if (r.getInputId().equals(inputId)) { - if (!isEmptySeriesRecording(r)) { - return true; - } - removedSeriesRecordings.add(r); + r -> { + if (r.getInputId().equals(inputId)) { + if (!isEmptySeriesRecording(r)) { + return true; } - return false; + removedSeriesRecordings.add(r); } + return false; }); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { @@ -898,8 +949,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { for (SeriesRecording r : removedSeriesRecordings) { mSeriesRecordingsForRemovedInput.remove(r.getId()); } - new AsyncDeleteSeriesRecordingTask(mContext) - .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings)); + ListenableFuture deleteSeriesRecordingFuture = + new DeleteSeriesRecordingFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + SeriesRecording.toArray(removedSeriesRecordings)); + mNoStopFuture.add(deleteSeriesRecordingFuture); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -918,32 +973,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { moveElements( mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter<ScheduledRecording>() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + r -> r.getInputId().equals(inputId)); List<SeriesRecording> movedSeriesRecordings = moveElements( mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + r -> r.getInputId().equals(inputId)); List<RecordedProgram> movedRecordedPrograms = moveElements( mRecordedPrograms, mRecordedProgramsForRemovedInput, - new Filter<RecordedProgram>() { - @Override - public boolean filter(RecordedProgram r) { - return r.getInputId().equals(inputId); - } - }); + r -> r.getInputId().equals(inputId)); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { mProgramId2ScheduledRecordings.remove(schedule.getProgramId()); @@ -1002,10 +1042,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { i.remove(); } } - new AsyncDeleteScheduleTask(mContext) - .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); - new AsyncDeleteSeriesRecordingTask(mContext) - .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete)); + ListenableFuture deleteScheduleFuture = + new DeleteScheduleFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + ScheduledRecording.toArray(schedulesToDelete)); + mNoStopFuture.add(deleteScheduleFuture); + ListenableFuture deleteSeriesRecordingFuture = + new DeleteSeriesRecordingFuture(mContext) + .executeOnDbThread( + removeFromSetOnCompletion, + SeriesRecording.toArray(seriesRecordingsToDelete)); + mNoStopFuture.add(deleteSeriesRecordingFuture); new AsyncDbTask<Void, Void, Void>(mDbExecutor) { @Override protected Void doInBackground(Void... params) { @@ -1036,7 +1084,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } if (!removedSeriesRecordings.isEmpty()) { SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed); + ListenableFuture deleteSeriesRecordingFuture = + new DeleteSeriesRecordingFuture(mContext) + .executeOnDbThread(removeFromSetOnCompletion, removed); + mNoStopFuture.add(deleteSeriesRecordingFuture); if (mDvrLoadFinished) { notifySeriesRecordingRemoved(removed); } @@ -1046,8 +1097,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; - public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) { - super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri); + public RecordedProgramsQueryTask(Uri uri) { + super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri); mUri = uri; } diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 63a245a3..cc9ad37a 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -29,6 +29,7 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.RemoteException; +import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -441,14 +442,7 @@ public class DvrManager { } synchronized (mListener) { for (final Entry<Listener, Handler> entry : mListener.entrySet()) { - entry.getValue() - .post( - new Runnable() { - @Override - public void run() { - entry.getKey().onStopRecordingRequested(recording); - } - }); + entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording)); } } } @@ -484,26 +478,26 @@ public class DvrManager { } /** Removes the recorded program. It deletes the file if possible. */ - public void removeRecordedProgram(Uri recordedProgramUri) { + public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; } - removeRecordedProgram(ContentUris.parseId(recordedProgramUri)); + removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile); } /** Removes the recorded program. It deletes the file if possible. */ - public void removeRecordedProgram(long recordedProgramId) { + public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; } RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId); if (recordedProgram != null) { - removeRecordedProgram(recordedProgram); + removeRecordedProgram(recordedProgram, deleteFile); } } /** Removes the recorded program. It deletes the file if possible. */ - public void removeRecordedProgram(final RecordedProgram recordedProgram) { + public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; } @@ -516,7 +510,7 @@ public class DvrManager { @Override protected void onPostExecute(Integer deletedCounts) { - if (deletedCounts > 0) { + if (deletedCounts > 0 && deleteFile) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { @@ -529,7 +523,7 @@ public class DvrManager { }.executeOnDbThread(); } - public void removeRecordedPrograms(List<Long> recordedProgramIds) { + public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) { final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>(); final List<Uri> dataUris = new ArrayList<>(); for (Long rId : recordedProgramIds) { @@ -554,7 +548,7 @@ public class DvrManager { @Override protected void onPostExecute(Boolean success) { - if (success) { + if (success && deleteFiles) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { @@ -829,37 +823,47 @@ public class DvrManager { @WorkerThread private void removeRecordedData(Uri dataUri) { try { - if (dataUri != null - && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) - && dataUri.getPath() != null) { + if (isFile(dataUri)) { File recordedProgramPath = new File(dataUri.getPath()); if (!recordedProgramPath.exists()) { if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath); } else { - CommonUtils.deleteDirOrFile(recordedProgramPath); - if (DEBUG) { - Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri); + if (CommonUtils.deleteDirOrFile(recordedProgramPath)) { + if (DEBUG) { + Log.d( + TAG, + "Successfully deleted files of the recorded program: " + + dataUri); + } + } else { + Log.w(TAG, "Unable to delete recording data at " + dataUri); } } } } catch (SecurityException e) { - if (DEBUG) { - Log.d( - TAG, - "To delete this recorded program, please manually delete video data at" - + "\nadb shell rm -rf " - + dataUri); - } + Log.w(TAG, "Unable to delete recording data at " + dataUri, e); } } + @AnyThread + public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) { + return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName()); + } + + @AnyThread + public static boolean isFile(Uri dataUri) { + return dataUri != null + && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) + && dataUri.getPath() != null; + } + /** * Remove all the records related to the input. * * <p>Note that this should be called after the input was removed. */ public void forgetStorage(String inputId) { - if (mDataManager.isInitialized()) { + if (mDataManager != null && mDataManager.isInitialized()) { mDataManager.forgetStorage(inputId); } } diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index d5126b12..7202dce0 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -923,12 +923,8 @@ public class DvrScheduleManager { List<ConflictInfo> result = new ArrayList<>(conflicts.values()); Collections.sort( result, - new Comparator<ConflictInfo>() { - @Override - public int compare(ConflictInfo lhs, ConflictInfo rhs) { - return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); - } - }); + (ConflictInfo lhs, ConflictInfo rhs) -> + RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule)); return result; } diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java index ed8d6903..dc347a9e 100644 --- a/src/com/android/tv/dvr/DvrStorageStatusManager.java +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -24,8 +24,9 @@ import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.RemoteException; -import android.support.media.tv.TvContractCompat; +import android.support.annotation.Nullable; import android.util.Log; +import androidx.tvprovider.media.tv.TvContractCompat; import com.android.tv.TvSingletons; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.util.CommonUtils; @@ -123,6 +124,8 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager { } } + + @Nullable private List<ContentProviderOperation> getDeleteOps() { List<ContentProviderOperation> ops = new ArrayList<>(); @@ -165,6 +168,9 @@ public class DvrStorageStatusManager extends RecordingStorageStatusManager { } } return ops; + } catch (Exception e) { + Log.w(TAG, "Error when getting delete ops at CleanUpDbTask", e); + return null; } } } diff --git a/src/com/android/tv/dvr/DvrTvView.java b/src/com/android/tv/dvr/DvrTvView.java new file mode 100644 index 00000000..be1f418b --- /dev/null +++ b/src/com/android/tv/dvr/DvrTvView.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2018 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.dvr; + +import android.content.Context; +import android.media.PlaybackParams; +import android.media.session.PlaybackState; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.TvViewSession; +import com.android.tv.TvSingletons; +import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; +import com.android.tv.dvr.ui.playback.DvrPlayer; +import com.android.tv.ui.AppLayerTvView; +import com.android.tv.ui.api.TunableTvViewPlayingApi; +import java.util.List; + +/** + * A {@link TvView} wrapper to handle events and TvView session. + */ +public class DvrTvView implements TunableTvViewPlayingApi { + + private final AppLayerTvView mTvView; + private DvrPlayer mDvrPlayer; + private String mInputId; + private Uri mRecordedProgramUri; + private TvInputCallbackCompat mTvInputCallbackCompat; + private InputSessionManager mInputSessionManager; + private TvViewSession mSession; + + public DvrTvView(Context context, AppLayerTvView tvView, DvrPlayer player) { + mTvView = tvView; + mDvrPlayer = player; + mInputSessionManager = TvSingletons.getSingletons(context).getInputSessionManager(); + } + + @Override + public boolean isPlaying() { + return mDvrPlayer.getPlaybackState() == PlaybackState.STATE_PLAYING; + } + + @Override + public void setStreamVolume(float volume) { + mTvView.setStreamVolume(volume); + } + + @Override + public void setTimeShiftListener(TimeShiftListener listener) { + // TimeShiftListener is never called from DvrTvView because TimeShift is always available + // and onRecordStartTimeChanged is not called during playback. + } + + @Override + public boolean isTimeShiftAvailable() { + return true; + } + + @Override + public void timeShiftPlay() { + if (mInputId != null && mRecordedProgramUri != null) { + mTvView.timeShiftPlay(mInputId, mRecordedProgramUri); + } + } + + public void timeShiftPlay(String inputId, Uri recordedProgramUri) { + mInputId = inputId; + mRecordedProgramUri = recordedProgramUri; + mSession.timeShiftPlay(inputId, recordedProgramUri); + } + + @Override + public void timeShiftPause() { + mTvView.timeShiftPause(); + } + + @Override + public void timeShiftRewind(int speed) { + PlaybackParams params = new PlaybackParams(); + params.setSpeed(speed * -1); + mTvView.timeShiftSetPlaybackParams(params); + } + + @Override + public void timeShiftFastForward(int speed) { + PlaybackParams params = new PlaybackParams(); + params.setSpeed(speed); + mTvView.timeShiftSetPlaybackParams(params); + } + + @Override + public void timeShiftSeekTo(long timeMs) { + mTvView.timeShiftSeekTo(timeMs); + } + + @Override + public long timeShiftGetCurrentPositionMs() { + return mDvrPlayer.getPlaybackPosition(); + } + + public void setCaptionEnabled(boolean enabled) { + mTvView.setCaptionEnabled(enabled); + } + + public void timeShiftResume() { + mTvView.timeShiftResume(); + } + + public void reset() { + mSession.reset(); + } + + public List<TvTrackInfo> getTracks(int type) { + return mTvView.getTracks(type); + } + + public void selectTrack(int type, String trackId) { + mTvView.selectTrack(type, trackId); + } + + public void timeShiftSetPlaybackParams(PlaybackParams params) { + mTvView.timeShiftSetPlaybackParams(params); + } + + public void setTimeShiftPositionCallback(@Nullable TvView.TimeShiftPositionCallback callback) { + mTvView.setTimeShiftPositionCallback(callback); + } + + public void setCallback(@Nullable TvInputCallbackCompat callback) { + mTvInputCallbackCompat = callback; + mTvView.setCallback(callback); + } + + public void init() { + mSession = mInputSessionManager.createTvViewSession(mTvView, this, mTvInputCallbackCompat); + } + + public void release() { + mInputSessionManager.releaseTvViewSession(mSession); + mInputSessionManager = null; + mDvrPlayer = null; + } +} diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java index e1fbca8c..899e65ac 100644 --- a/src/com/android/tv/dvr/data/RecordedProgram.java +++ b/src/com/android/tv/dvr/data/RecordedProgram.java @@ -22,31 +22,38 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContentRating; -import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs.Genres; import android.media.tv.TvContract.RecordedPrograms; import android.net.Uri; import android.os.Build; +import android.support.annotation.CheckResult; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import android.text.TextUtils; +import android.util.Log; import com.android.tv.common.R; import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.data.RecordedProgramState; import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.StringUtils; import com.android.tv.data.BaseProgram; import com.android.tv.data.GenreItems; import com.android.tv.data.InternalDataUtils; -import java.util.Arrays; +import com.android.tv.util.TvProviderUtils; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import java.util.Collection; import java.util.Comparator; -import java.util.Objects; import java.util.concurrent.TimeUnit; /** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */ @TargetApi(Build.VERSION_CODES.N) -public class RecordedProgram extends BaseProgram { +@AutoValue +public abstract class RecordedProgram extends BaseProgram { public static final int ID_NOT_SET = -1; + private static final String TAG = "RecordedProgram"; public static final String[] PROJECTION = { - // These are in exactly the order listed in RecordedPrograms RecordedPrograms._ID, RecordedPrograms.COLUMN_PACKAGE_NAME, RecordedPrograms.COLUMN_INPUT_ID, @@ -73,10 +80,6 @@ public class RecordedProgram extends BaseProgram { RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, RecordedPrograms.COLUMN_VERSION_NUMBER, RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, }; @@ -89,834 +92,372 @@ public class RecordedProgram extends BaseProgram { .setPackageName(cursor.getString(index++)) .setInputId(cursor.getString(index++)) .setChannelId(cursor.getLong(index++)) - .setTitle(cursor.getString(index++)) - .setSeasonNumber(cursor.getString(index++)) - .setSeasonTitle(cursor.getString(index++)) - .setEpisodeNumber(cursor.getString(index++)) - .setEpisodeTitle(cursor.getString(index++)) + .setTitle(StringUtils.nullToEmpty(cursor.getString(index++))) + .setSeasonNumber(StringUtils.nullToEmpty(cursor.getString(index++))) + .setSeasonTitle(StringUtils.nullToEmpty(cursor.getString(index++))) + .setEpisodeNumber(StringUtils.nullToEmpty(cursor.getString(index++))) + .setEpisodeTitle(StringUtils.nullToEmpty(cursor.getString(index++))) .setStartTimeUtcMillis(cursor.getLong(index++)) .setEndTimeUtcMillis(cursor.getLong(index++)) .setBroadcastGenres(cursor.getString(index++)) .setCanonicalGenres(cursor.getString(index++)) - .setShortDescription(cursor.getString(index++)) - .setLongDescription(cursor.getString(index++)) + .setDescription(StringUtils.nullToEmpty(cursor.getString(index++))) + .setLongDescription(StringUtils.nullToEmpty(cursor.getString(index++))) .setVideoWidth(cursor.getInt(index++)) .setVideoHeight(cursor.getInt(index++)) - .setAudioLanguage(cursor.getString(index++)) + .setAudioLanguage(StringUtils.nullToEmpty(cursor.getString(index++))) .setContentRatings( TvContentRatingCache.getInstance() .getRatings(cursor.getString(index++))) - .setPosterArtUri(cursor.getString(index++)) - .setThumbnailUri(cursor.getString(index++)) + .setPosterArtUri(StringUtils.nullToEmpty(cursor.getString(index++))) + .setThumbnailUri(StringUtils.nullToEmpty(cursor.getString(index++))) .setSearchable(cursor.getInt(index++) == 1) .setDataUri(cursor.getString(index++)) .setDataBytes(cursor.getLong(index++)) .setDurationMillis(cursor.getLong(index++)) .setExpireTimeUtcMillis(cursor.getLong(index++)) - .setInternalProviderFlag1(cursor.getInt(index++)) - .setInternalProviderFlag2(cursor.getInt(index++)) - .setInternalProviderFlag3(cursor.getInt(index++)) - .setInternalProviderFlag4(cursor.getInt(index++)) .setVersionNumber(cursor.getInt(index++)); - if (CommonUtils.isInBundledPackageSet(builder.mPackageName)) { + if (CommonUtils.isInBundledPackageSet(builder.getPackageName())) { InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); } + index++; + if (TvProviderUtils.getRecordedProgramHasSeriesIdColumn()) { + builder.setSeriesId(StringUtils.nullToEmpty(cursor.getString(index++))); + } + if (TvProviderUtils.getRecordedProgramHasStateColumn()) { + builder.setState(cursor.getString(index++)); + } return builder.build(); } - public static ContentValues toValues(RecordedProgram recordedProgram) { + @WorkerThread + public static ContentValues toValues(Context context, RecordedProgram recordedProgram) { ContentValues values = new ContentValues(); - if (recordedProgram.mId != ID_NOT_SET) { - values.put(RecordedPrograms._ID, recordedProgram.mId); + if (recordedProgram.getId() != ID_NOT_SET) { + values.put(RecordedPrograms._ID, recordedProgram.getId()); } - values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId); - values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId); - values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle); - values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber); - values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle); - values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber); - values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle); + values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.getInputId()); + values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.getChannelId()); + values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.getTitle()); + values.put( + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.getSeasonNumber()); + values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.getSeasonTitle()); + values.put( + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.getEpisodeNumber()); + values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.getEpisodeTitle()); + values.put( + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + recordedProgram.getStartTimeUtcMillis()); values.put( - RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, recordedProgram.mStartTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis); + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.getEndTimeUtcMillis()); values.put( RecordedPrograms.COLUMN_BROADCAST_GENRE, - safeEncode(recordedProgram.mBroadcastGenres)); + safeEncode(recordedProgram.getBroadcastGenres())); values.put( RecordedPrograms.COLUMN_CANONICAL_GENRE, - safeEncode(recordedProgram.mCanonicalGenres)); - values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription); - values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription); - if (recordedProgram.mVideoWidth == 0) { + safeEncode(recordedProgram.getCanonicalGenres())); + values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.getDescription()); + values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.getLongDescription()); + if (recordedProgram.getVideoWidth() == 0) { values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH); } else { - values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth); + values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.getVideoWidth()); } - if (recordedProgram.mVideoHeight == 0) { + if (recordedProgram.getVideoHeight() == 0) { values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT); } else { - values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); + values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.getVideoHeight()); } - values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); + values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.getAudioLanguage()); values.put( RecordedPrograms.COLUMN_CONTENT_RATING, - TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings)); - values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri); - values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri); - values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0); + TvContentRatingCache.contentRatingsToString(recordedProgram.getContentRatings())); + values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.getPosterArtUri()); + values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.getThumbnailUri()); + values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.isSearchable() ? 1 : 0); values.put( - RecordedPrograms.COLUMN_RECORDING_DATA_URI, safeToString(recordedProgram.mDataUri)); - values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes); + RecordedPrograms.COLUMN_RECORDING_DATA_URI, + safeToString(recordedProgram.getDataUri())); + values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.getDataBytes()); values.put( - RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, recordedProgram.mDurationMillis); + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + recordedProgram.getDurationMillis()); values.put( RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, - recordedProgram.mExpireTimeUtcMillis); + recordedProgram.getExpireTimeUtcMillis()); values.put( RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(recordedProgram)); - values.put( - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, - recordedProgram.mInternalProviderFlag1); - values.put( - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, - recordedProgram.mInternalProviderFlag2); - values.put( - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, - recordedProgram.mInternalProviderFlag3); - values.put( - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, - recordedProgram.mInternalProviderFlag4); - values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber); + values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.getVersionNumber()); + if (TvProviderUtils.checkSeriesIdColumn(context, RecordedPrograms.CONTENT_URI)) { + values.put(COLUMN_SERIES_ID, recordedProgram.getSeriesId()); + } + if (TvProviderUtils.checkStateColumn(context, RecordedPrograms.CONTENT_URI)) { + values.put(COLUMN_STATE, recordedProgram.getState().toString()); + } return values; } - public static class Builder { - private long mId = ID_NOT_SET; - private String mPackageName; - private String mInputId; - private long mChannelId; - private String mTitle; - private String mSeriesId; - private String mSeasonNumber; - private String mSeasonTitle; - private String mEpisodeNumber; - private String mEpisodeTitle; - private long mStartTimeUtcMillis; - private long mEndTimeUtcMillis; - private String[] mBroadcastGenres; - private String[] mCanonicalGenres; - private String mShortDescription; - private String mLongDescription; - private int mVideoWidth; - private int mVideoHeight; - private String mAudioLanguage; - private TvContentRating[] mContentRatings; - private String mPosterArtUri; - private String mThumbnailUri; - private boolean mSearchable = true; - private Uri mDataUri; - private long mDataBytes; - private long mDurationMillis; - private long mExpireTimeUtcMillis; - private int mInternalProviderFlag1; - private int mInternalProviderFlag2; - private int mInternalProviderFlag3; - private int mInternalProviderFlag4; - private int mVersionNumber; - - public Builder setId(long id) { - mId = id; - return this; - } + /** Builder for {@link RecordedProgram}s. */ + @AutoValue.Builder + public abstract static class Builder { - public Builder setPackageName(String packageName) { - mPackageName = packageName; - return this; - } + public abstract Builder setId(long id); - public Builder setInputId(String inputId) { - mInputId = inputId; - return this; - } + public abstract Builder setPackageName(String packageName); - public Builder setChannelId(long channelId) { - mChannelId = channelId; - return this; - } + abstract String getPackageName(); - public Builder setTitle(String title) { - mTitle = title; - return this; - } + public abstract Builder setInputId(String inputId); - public Builder setSeriesId(String seriesId) { - mSeriesId = seriesId; - return this; - } + public abstract Builder setChannelId(long channelId); - public Builder setSeasonNumber(String seasonNumber) { - mSeasonNumber = seasonNumber; - return this; - } + abstract String getTitle(); - public Builder setSeasonTitle(String seasonTitle) { - mSeasonTitle = seasonTitle; - return this; - } + public abstract Builder setTitle(String title); - public Builder setEpisodeNumber(String episodeNumber) { - mEpisodeNumber = episodeNumber; - return this; - } + abstract String getSeriesId(); - public Builder setEpisodeTitle(String episodeTitle) { - mEpisodeTitle = episodeTitle; - return this; - } + public abstract Builder setSeriesId(String seriesId); - public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { - mStartTimeUtcMillis = startTimeUtcMillis; - return this; - } + public abstract Builder setSeasonNumber(String seasonNumber); - public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { - mEndTimeUtcMillis = endTimeUtcMillis; - return this; - } + public abstract Builder setSeasonTitle(String seasonTitle); + + @Nullable + abstract String getEpisodeNumber(); + + public abstract Builder setEpisodeNumber(String episodeNumber); - public Builder setBroadcastGenres(String broadcastGenres) { - if (TextUtils.isEmpty(broadcastGenres)) { - mBroadcastGenres = null; - return this; + public abstract Builder setEpisodeTitle(String episodeTitle); + + public abstract Builder setStartTimeUtcMillis(long startTimeUtcMillis); + + public abstract Builder setEndTimeUtcMillis(long endTimeUtcMillis); + + public abstract Builder setState(RecordedProgramState state); + + public Builder setState(@Nullable String state) { + + if (!TextUtils.isEmpty(state)) { + try { + return setState(RecordedProgramState.valueOf(state)); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Unknown recording state " + state, e); + } } - return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres)); + return setState(RecordedProgramState.NOT_SET); } - private Builder setBroadcastGenres(String[] broadcastGenres) { - mBroadcastGenres = broadcastGenres; - return this; + public Builder setBroadcastGenres(@Nullable String broadcastGenres) { + return setBroadcastGenres( + TextUtils.isEmpty(broadcastGenres) + ? ImmutableList.of() + : ImmutableList.copyOf(Genres.decode(broadcastGenres))); } + public abstract Builder setBroadcastGenres(ImmutableList<String> broadcastGenres); + public Builder setCanonicalGenres(String canonicalGenres) { - if (TextUtils.isEmpty(canonicalGenres)) { - mCanonicalGenres = null; - return this; - } - return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres)); + return setCanonicalGenres( + TextUtils.isEmpty(canonicalGenres) + ? ImmutableList.of() + : ImmutableList.copyOf(Genres.decode(canonicalGenres))); } - private Builder setCanonicalGenres(String[] canonicalGenres) { - mCanonicalGenres = canonicalGenres; - return this; - } + public abstract Builder setCanonicalGenres(ImmutableList<String> canonicalGenres); - public Builder setShortDescription(String shortDescription) { - mShortDescription = shortDescription; - return this; - } + public abstract Builder setDescription(String shortDescription); - public Builder setLongDescription(String longDescription) { - mLongDescription = longDescription; - return this; - } + public abstract Builder setLongDescription(String longDescription); - public Builder setVideoWidth(int videoWidth) { - mVideoWidth = videoWidth; - return this; - } + public abstract Builder setVideoWidth(int videoWidth); - public Builder setVideoHeight(int videoHeight) { - mVideoHeight = videoHeight; - return this; - } + public abstract Builder setVideoHeight(int videoHeight); - public Builder setAudioLanguage(String audioLanguage) { - mAudioLanguage = audioLanguage; - return this; - } + public abstract Builder setAudioLanguage(String audioLanguage); - public Builder setContentRatings(TvContentRating[] contentRatings) { - mContentRatings = contentRatings; - return this; - } + public abstract Builder setContentRatings(ImmutableList<TvContentRating> contentRatings); - private Uri toUri(String uriString) { + private Uri toUri(@Nullable String uriString) { try { return uriString == null ? null : Uri.parse(uriString); } catch (Exception e) { - return null; + return Uri.EMPTY; } } - public Builder setPosterArtUri(String posterArtUri) { - mPosterArtUri = posterArtUri; - return this; - } + public abstract Builder setPosterArtUri(String posterArtUri); - public Builder setThumbnailUri(String thumbnailUri) { - mThumbnailUri = thumbnailUri; - return this; - } + public abstract Builder setThumbnailUri(String thumbnailUri); - public Builder setSearchable(boolean searchable) { - mSearchable = searchable; - return this; - } + public abstract Builder setSearchable(boolean searchable); - public Builder setDataUri(String dataUri) { + public Builder setDataUri(@Nullable String dataUri) { return setDataUri(toUri(dataUri)); } - public Builder setDataUri(Uri dataUri) { - mDataUri = dataUri; - return this; - } - - public Builder setDataBytes(long dataBytes) { - mDataBytes = dataBytes; - return this; - } - - public Builder setDurationMillis(long durationMillis) { - mDurationMillis = durationMillis; - return this; - } - - public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) { - mExpireTimeUtcMillis = expireTimeUtcMillis; - return this; - } + public abstract Builder setDataUri(Uri dataUri); - public Builder setInternalProviderFlag1(int internalProviderFlag1) { - mInternalProviderFlag1 = internalProviderFlag1; - return this; - } + public abstract Builder setDataBytes(long dataBytes); - public Builder setInternalProviderFlag2(int internalProviderFlag2) { - mInternalProviderFlag2 = internalProviderFlag2; - return this; - } + public abstract Builder setDurationMillis(long durationMillis); - public Builder setInternalProviderFlag3(int internalProviderFlag3) { - mInternalProviderFlag3 = internalProviderFlag3; - return this; - } + public abstract Builder setExpireTimeUtcMillis(long expireTimeUtcMillis); - public Builder setInternalProviderFlag4(int internalProviderFlag4) { - mInternalProviderFlag4 = internalProviderFlag4; - return this; - } + public abstract Builder setVersionNumber(int versionNumber); - public Builder setVersionNumber(int versionNumber) { - mVersionNumber = versionNumber; - return this; - } + abstract RecordedProgram autoBuild(); public RecordedProgram build() { - if (TextUtils.isEmpty(mTitle)) { + if (TextUtils.isEmpty(getTitle())) { // If title is null, series cannot be generated for this program. setSeriesId(null); - } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) { + } else if (TextUtils.isEmpty(getSeriesId()) && !TextUtils.isEmpty(getEpisodeNumber())) { // If series ID is not set, generate it for the episodic program of other TV input. - setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle)); + setSeriesId(BaseProgram.generateSeriesId(getPackageName(), getTitle())); } - return new RecordedProgram( - mId, - mPackageName, - mInputId, - mChannelId, - mTitle, - mSeriesId, - mSeasonNumber, - mSeasonTitle, - mEpisodeNumber, - mEpisodeTitle, - mStartTimeUtcMillis, - mEndTimeUtcMillis, - mBroadcastGenres, - mCanonicalGenres, - mShortDescription, - mLongDescription, - mVideoWidth, - mVideoHeight, - mAudioLanguage, - mContentRatings, - mPosterArtUri, - mThumbnailUri, - mSearchable, - mDataUri, - mDataBytes, - mDurationMillis, - mExpireTimeUtcMillis, - mInternalProviderFlag1, - mInternalProviderFlag2, - mInternalProviderFlag3, - mInternalProviderFlag4, - mVersionNumber); + return (autoBuild()); } } public static Builder builder() { - return new Builder(); - } - - public static Builder buildFrom(RecordedProgram orig) { - return builder() - .setId(orig.getId()) - .setPackageName(orig.getPackageName()) - .setInputId(orig.getInputId()) - .setChannelId(orig.getChannelId()) - .setTitle(orig.getTitle()) - .setSeriesId(orig.getSeriesId()) - .setSeasonNumber(orig.getSeasonNumber()) - .setSeasonTitle(orig.getSeasonTitle()) - .setEpisodeNumber(orig.getEpisodeNumber()) - .setEpisodeTitle(orig.getEpisodeTitle()) - .setStartTimeUtcMillis(orig.getStartTimeUtcMillis()) - .setEndTimeUtcMillis(orig.getEndTimeUtcMillis()) - .setBroadcastGenres(orig.getBroadcastGenres()) - .setCanonicalGenres(orig.getCanonicalGenres()) - .setShortDescription(orig.getDescription()) - .setLongDescription(orig.getLongDescription()) - .setVideoWidth(orig.getVideoWidth()) - .setVideoHeight(orig.getVideoHeight()) - .setAudioLanguage(orig.getAudioLanguage()) - .setContentRatings(orig.getContentRatings()) - .setPosterArtUri(orig.getPosterArtUri()) - .setThumbnailUri(orig.getThumbnailUri()) - .setSearchable(orig.isSearchable()) - .setInternalProviderFlag1(orig.getInternalProviderFlag1()) - .setInternalProviderFlag2(orig.getInternalProviderFlag2()) - .setInternalProviderFlag3(orig.getInternalProviderFlag3()) - .setInternalProviderFlag4(orig.getInternalProviderFlag4()) - .setVersionNumber(orig.getVersionNumber()); + return new AutoValue_RecordedProgram.Builder() + .setId(ID_NOT_SET) + .setChannelId(ID_NOT_SET) + .setAudioLanguage("") + .setBroadcastGenres("") + .setCanonicalGenres("") + .setContentRatings(ImmutableList.of()) + .setDataUri("") + .setDurationMillis(0) + .setDescription("") + .setDataBytes(0) + .setLongDescription("") + .setEndTimeUtcMillis(0) + .setEpisodeNumber("") + .setEpisodeTitle("") + .setExpireTimeUtcMillis(0) + .setPackageName("") + .setPosterArtUri("") + .setSeasonNumber("") + .setSeasonTitle("") + .setSearchable(false) + .setSeriesId("") + .setStartTimeUtcMillis(0) + .setState(RecordedProgramState.NOT_SET) + .setThumbnailUri("") + .setTitle("") + .setVersionNumber(0) + .setVideoHeight(0) + .setVideoWidth(0); } public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR = - new Comparator<RecordedProgram>() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - int res = - Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis()); - if (res != 0) { - return res; - } - return Long.compare(lhs.mId, rhs.mId); + (RecordedProgram lhs, RecordedProgram rhs) -> { + int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis()); + if (res != 0) { + return res; } + return Long.compare(lhs.getId(), rhs.getId()); }; private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); - private final long mId; - private final String mPackageName; - private final String mInputId; - private final long mChannelId; - private final String mTitle; - private final String mSeriesId; - private final String mSeasonNumber; - private final String mSeasonTitle; - private final String mEpisodeNumber; - private final String mEpisodeTitle; - private final long mStartTimeUtcMillis; - private final long mEndTimeUtcMillis; - private final String[] mBroadcastGenres; - private final String[] mCanonicalGenres; - private final String mShortDescription; - private final String mLongDescription; - private final int mVideoWidth; - private final int mVideoHeight; - private final String mAudioLanguage; - private final TvContentRating[] mContentRatings; - private final String mPosterArtUri; - private final String mThumbnailUri; - private final boolean mSearchable; - private final Uri mDataUri; - private final long mDataBytes; - private final long mDurationMillis; - private final long mExpireTimeUtcMillis; - private final int mInternalProviderFlag1; - private final int mInternalProviderFlag2; - private final int mInternalProviderFlag3; - private final int mInternalProviderFlag4; - private final int mVersionNumber; - - private RecordedProgram( - long id, - String packageName, - String inputId, - long channelId, - String title, - String seriesId, - String seasonNumber, - String seasonTitle, - String episodeNumber, - String episodeTitle, - long startTimeUtcMillis, - long endTimeUtcMillis, - String[] broadcastGenres, - String[] canonicalGenres, - String shortDescription, - String longDescription, - int videoWidth, - int videoHeight, - String audioLanguage, - TvContentRating[] contentRatings, - String posterArtUri, - String thumbnailUri, - boolean searchable, - Uri dataUri, - long dataBytes, - long durationMillis, - long expireTimeUtcMillis, - int internalProviderFlag1, - int internalProviderFlag2, - int internalProviderFlag3, - int internalProviderFlag4, - int versionNumber) { - mId = id; - mPackageName = packageName; - mInputId = inputId; - mChannelId = channelId; - mTitle = title; - mSeriesId = seriesId; - mSeasonNumber = seasonNumber; - mSeasonTitle = seasonTitle; - mEpisodeNumber = episodeNumber; - mEpisodeTitle = episodeTitle; - mStartTimeUtcMillis = startTimeUtcMillis; - mEndTimeUtcMillis = endTimeUtcMillis; - mBroadcastGenres = broadcastGenres; - mCanonicalGenres = canonicalGenres; - mShortDescription = shortDescription; - mLongDescription = longDescription; - mVideoWidth = videoWidth; - mVideoHeight = videoHeight; - - mAudioLanguage = audioLanguage; - mContentRatings = contentRatings; - mPosterArtUri = posterArtUri; - mThumbnailUri = thumbnailUri; - mSearchable = searchable; - mDataUri = dataUri; - mDataBytes = dataBytes; - mDurationMillis = durationMillis; - mExpireTimeUtcMillis = expireTimeUtcMillis; - mInternalProviderFlag1 = internalProviderFlag1; - mInternalProviderFlag2 = internalProviderFlag2; - mInternalProviderFlag3 = internalProviderFlag3; - mInternalProviderFlag4 = internalProviderFlag4; - mVersionNumber = versionNumber; - } - - public String getAudioLanguage() { - return mAudioLanguage; - } + public abstract String getAudioLanguage(); - public String[] getBroadcastGenres() { - return mBroadcastGenres; - } + public abstract ImmutableList<String> getBroadcastGenres(); - public String[] getCanonicalGenres() { - return mCanonicalGenres; - } + public abstract ImmutableList<String> getCanonicalGenres(); /** Returns array of canonical genre ID's for this recorded program. */ @Override public int[] getCanonicalGenreIds() { - if (mCanonicalGenres == null) { - return null; - } - int[] genreIds = new int[mCanonicalGenres.length]; - for (int i = 0; i < mCanonicalGenres.length; i++) { - genreIds[i] = GenreItems.getId(mCanonicalGenres[i]); + + ImmutableList<String> canonicalGenres = getCanonicalGenres(); + int[] genreIds = new int[getCanonicalGenres().size()]; + for (int i = 0; i < canonicalGenres.size(); i++) { + genreIds[i] = GenreItems.getId(canonicalGenres.get(i)); } return genreIds; } - @Override - public long getChannelId() { - return mChannelId; - } - - @Nullable - @Override - public TvContentRating[] getContentRatings() { - return mContentRatings; - } - - public Uri getDataUri() { - return mDataUri; - } - - public long getDataBytes() { - return mDataBytes; - } - - @Override - public long getDurationMillis() { - return mDurationMillis; - } - - @Override - public long getEndTimeUtcMillis() { - return mEndTimeUtcMillis; - } - - @Override - public String getEpisodeNumber() { - return mEpisodeNumber; - } + public abstract Uri getDataUri(); - @Override - public String getEpisodeTitle() { - return mEpisodeTitle; - } + public abstract long getDataBytes(); @Nullable public String getEpisodeDisplayNumber(Context context) { - if (!TextUtils.isEmpty(mEpisodeNumber)) { - if (TextUtils.equals(mSeasonNumber, "0")) { + if (!TextUtils.isEmpty(getEpisodeNumber())) { + if (TextUtils.equals(getSeasonNumber(), "0")) { // Do not show "S0: ". - return String.format( - context.getResources() - .getString(R.string.display_episode_number_format_no_season_number), - mEpisodeNumber); + return context.getResources() + .getString( + R.string.display_episode_number_format_no_season_number, + getEpisodeNumber()); } else { - return String.format( - context.getResources().getString(R.string.display_episode_number_format), - mSeasonNumber, - mEpisodeNumber); + return context.getResources() + .getString( + R.string.display_episode_number_format, + getSeasonNumber(), + getEpisodeNumber()); } } return null; } - public long getExpireTimeUtcMillis() { - return mExpireTimeUtcMillis; - } - - public long getId() { - return mId; - } - - public String getPackageName() { - return mPackageName; - } - - public String getInputId() { - return mInputId; - } - - public int getInternalProviderFlag1() { - return mInternalProviderFlag1; - } - - public int getInternalProviderFlag2() { - return mInternalProviderFlag2; - } - - public int getInternalProviderFlag3() { - return mInternalProviderFlag3; - } - - public int getInternalProviderFlag4() { - return mInternalProviderFlag4; - } - - @Override - public String getDescription() { - return mShortDescription; - } + public abstract long getExpireTimeUtcMillis(); - @Override - public String getLongDescription() { - return mLongDescription; - } + public abstract String getPackageName(); - @Override - public String getPosterArtUri() { - return mPosterArtUri; - } + public abstract String getInputId(); @Override public boolean isValid() { return true; } - public boolean isSearchable() { - return mSearchable; - } - - @Override - public String getSeriesId() { - return mSeriesId; - } - - @Override - public String getSeasonNumber() { - return mSeasonNumber; + public boolean isVisible() { + switch (getState()) { + case NOT_SET: + case FINISHED: + return true; + default: + return false; + } } - public String getSeasonTitle() { - return mSeasonTitle; + public boolean isPartial() { + return getState() == RecordedProgramState.PARTIAL; } - @Override - public long getStartTimeUtcMillis() { - return mStartTimeUtcMillis; - } + public abstract boolean isSearchable(); - @Override - public String getThumbnailUri() { - return mThumbnailUri; - } + public abstract String getSeasonTitle(); - @Override - public String getTitle() { - return mTitle; - } + public abstract RecordedProgramState getState(); public Uri getUri() { - return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId); + return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, getId()); } - public int getVersionNumber() { - return mVersionNumber; - } + public abstract int getVersionNumber(); - public int getVideoHeight() { - return mVideoHeight; - } + public abstract int getVideoHeight(); - public int getVideoWidth() { - return mVideoWidth; - } + public abstract int getVideoWidth(); /** Checks whether the recording has been clipped or not. */ public boolean isClipped() { - return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RecordedProgram that = (RecordedProgram) o; - return Objects.equals(mId, that.mId) - && Objects.equals(mChannelId, that.mChannelId) - && Objects.equals(mSeriesId, that.mSeriesId) - && Objects.equals(mSeasonNumber, that.mSeasonNumber) - && Objects.equals(mSeasonTitle, that.mSeasonTitle) - && Objects.equals(mEpisodeNumber, that.mEpisodeNumber) - && Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) - && Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) - && Objects.equals(mVideoWidth, that.mVideoWidth) - && Objects.equals(mVideoHeight, that.mVideoHeight) - && Objects.equals(mSearchable, that.mSearchable) - && Objects.equals(mDataBytes, that.mDataBytes) - && Objects.equals(mDurationMillis, that.mDurationMillis) - && Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) - && Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) - && Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) - && Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) - && Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) - && Objects.equals(mVersionNumber, that.mVersionNumber) - && Objects.equals(mTitle, that.mTitle) - && Objects.equals(mEpisodeTitle, that.mEpisodeTitle) - && Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) - && Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) - && Objects.equals(mShortDescription, that.mShortDescription) - && Objects.equals(mLongDescription, that.mLongDescription) - && Objects.equals(mAudioLanguage, that.mAudioLanguage) - && Arrays.equals(mContentRatings, that.mContentRatings) - && Objects.equals(mPosterArtUri, that.mPosterArtUri) - && Objects.equals(mThumbnailUri, that.mThumbnailUri); + return getEndTimeUtcMillis() - getStartTimeUtcMillis() - getDurationMillis() + > CLIPPED_THRESHOLD_MS; } - /** Hashes based on the ID. */ - @Override - public int hashCode() { - return Objects.hash(mId); - } + public abstract Builder toBuilder(); - @Override - public String toString() { - return "RecordedProgram" - + "[" - + mId - + "]{ mPackageName=" - + mPackageName - + ", mInputId='" - + mInputId - + '\'' - + ", mChannelId='" - + mChannelId - + '\'' - + ", mTitle='" - + mTitle - + '\'' - + ", mSeriesId='" - + mSeriesId - + '\'' - + ", mEpisodeNumber=" - + mEpisodeNumber - + ", mEpisodeTitle='" - + mEpisodeTitle - + '\'' - + ", mStartTimeUtcMillis=" - + mStartTimeUtcMillis - + ", mEndTimeUtcMillis=" - + mEndTimeUtcMillis - + ", mBroadcastGenres=" - + (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") - + ", mCanonicalGenres=" - + (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") - + ", mShortDescription='" - + mShortDescription - + '\'' - + ", mLongDescription='" - + mLongDescription - + '\'' - + ", mVideoHeight=" - + mVideoHeight - + ", mVideoWidth=" - + mVideoWidth - + ", mAudioLanguage='" - + mAudioLanguage - + '\'' - + ", mContentRatings='" - + TvContentRatingCache.contentRatingsToString(mContentRatings) - + '\'' - + ", mPosterArtUri=" - + mPosterArtUri - + ", mThumbnailUri=" - + mThumbnailUri - + ", mSearchable=" - + mSearchable - + ", mDataUri=" - + mDataUri - + ", mDataBytes=" - + mDataBytes - + ", mDurationMillis=" - + mDurationMillis - + ", mExpireTimeUtcMillis=" - + mExpireTimeUtcMillis - + ", mInternalProviderFlag1=" - + mInternalProviderFlag1 - + ", mInternalProviderFlag2=" - + mInternalProviderFlag2 - + ", mInternalProviderFlag3=" - + mInternalProviderFlag3 - + ", mInternalProviderFlag4=" - + mInternalProviderFlag4 - + ", mSeasonNumber=" - + mSeasonNumber - + ", mSeasonTitle=" - + mSeasonTitle - + ", mVersionNumber=" - + mVersionNumber - + '}'; + @CheckResult + public RecordedProgram withId(long id) { + return toBuilder().setId(id).build(); } @Nullable @@ -925,8 +466,8 @@ public class RecordedProgram extends BaseProgram { } @Nullable - private static String safeEncode(@Nullable String[] genres) { - return genres == null ? null : TvContract.Programs.Genres.encode(genres); + private static String safeEncode(@Nullable ImmutableList<String> genres) { + return genres == null ? null : Genres.encode(genres.toArray(new String[0])); } /** Returns an array containing all of the elements in the list. */ diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java index 7c2d12d9..ba6d3cf9 100644 --- a/src/com/android/tv/dvr/data/ScheduledRecording.java +++ b/src/com/android/tv/dvr/data/ScheduledRecording.java @@ -56,39 +56,22 @@ public final class ScheduledRecording implements Parcelable { /** Compares the start time in ascending order. */ public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = - new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); - } - }; + (ScheduledRecording lhs, ScheduledRecording rhs) -> + Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); /** Compares the end time in ascending order. */ public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR = - new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); - } - }; + (ScheduledRecording lhs, ScheduledRecording rhs) -> + Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); /** Compares ID in ascending order. The schedule with the larger ID was created later. */ public static final Comparator<ScheduledRecording> ID_COMPARATOR = - new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mId, rhs.mId); - } - }; + (ScheduledRecording lhs, ScheduledRecording rhs) -> Long.compare(lhs.mId, rhs.mId); /** Compares the priority in ascending order. */ public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = - new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mPriority, rhs.mPriority); - } - }; + (ScheduledRecording lhs, ScheduledRecording rhs) -> + Long.compare(lhs.mPriority, rhs.mPriority); /** * Compares start time in ascending order and then priority in descending order and then ID in @@ -359,15 +342,22 @@ public final class ScheduledRecording implements Parcelable { }) public @interface RecordingFailedReason {} + // next number for failed reason: 11 public static final int FAILED_REASON_OTHER = 0; - public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1; public static final int FAILED_REASON_NOT_FINISHED = 2; public static final int FAILED_REASON_SCHEDULER_STOPPED = 3; public static final int FAILED_REASON_INVALID_CHANNEL = 4; public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5; public static final int FAILED_REASON_CONNECTION_FAILED = 6; + + // for the following reasons, show advice to users + // TODO(b/72638597): add failure condition of "weak signal" + + // failed reason is FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED when tuner or external + // storage is disconnected + public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1; + // failed reason is FAILED_REASON_RESOURCE_BUSY when antenna is disconnected or signal is weak public static final int FAILED_REASON_RESOURCE_BUSY = 7; - // For the following reasons, show advice to users public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8; public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9; public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10; @@ -679,7 +669,8 @@ public final class ScheduledRecording implements Parcelable { } /** Returns the failed reason of the {@link ScheduledRecording}. */ - @Nullable @RecordingFailedReason + @Nullable + @RecordingFailedReason public Integer getFailedReason() { return mFailedReason; } @@ -812,10 +803,7 @@ public final class ScheduledRecording implements Parcelable { } } - /** - * Converts a string to a failed reason integer, defaulting to {@link - * #FAILED_REASON_OTHER}. - */ + /** Converts a string to a failed reason integer, defaulting to {@link #FAILED_REASON_OTHER}. */ private static Integer recordingFailedReason(String reason) { if (TextUtils.isEmpty(reason)) { return null; @@ -985,6 +973,11 @@ public final class ScheduledRecording implements Parcelable { return mState == STATE_RECORDING_FINISHED; } + /** Returns {@code true} if the recording is failed, otherwise @{code false}. */ + public boolean isFailed() { + return mState == STATE_RECORDING_FAILED; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof ScheduledRecording)) { diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java index 96b3425a..6cb0e836 100644 --- a/src/com/android/tv/dvr/data/SeriesRecording.java +++ b/src/com/android/tv/dvr/data/SeriesRecording.java @@ -49,9 +49,8 @@ public class SeriesRecording implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL} - ) + flag = true, + value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}) public @interface ChannelOption {} /** An option which indicates that the episodes in one channel are recorded. */ public static final int OPTION_CHANNEL_ONE = 0; @@ -60,9 +59,8 @@ public class SeriesRecording implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED} - ) + flag = true, + value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) public @interface SeriesState {} /** The state indicates that the series recording is a normal one. */ @@ -73,26 +71,18 @@ public class SeriesRecording implements Parcelable { /** Compare priority in descending order. */ public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR = - new Comparator<SeriesRecording>() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - int value = Long.compare(rhs.mPriority, lhs.mPriority); - if (value == 0) { - // New recording has the higher priority. - value = Long.compare(rhs.mId, lhs.mId); - } - return value; + (SeriesRecording lhs, SeriesRecording rhs) -> { + int value = Long.compare(rhs.mPriority, lhs.mPriority); + if (value == 0) { + // New recording has the higher priority. + value = Long.compare(rhs.mId, lhs.mId); } + return value; }; /** Compare ID in ascending order. */ public static final Comparator<SeriesRecording> ID_COMPARATOR = - new Comparator<SeriesRecording>() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - return Long.compare(lhs.mId, rhs.mId); - } - }; + (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId); /** * Creates a new Builder with the values set from the series information of {@link BaseProgram}. diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 41e5a66a..ebf133db 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -79,6 +79,8 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { + " TEXT," + Schedules.COLUMN_STATE + " TEXT NOT NULL," + + Schedules.COLUMN_FAILED_REASON + + " TEXT," + Schedules.COLUMN_SERIES_RECORDING_ID + " INTEGER," + "FOREIGN KEY(" @@ -261,6 +263,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS); db.execSQL(SQL_DROP_SERIES_RECORDINGS); onCreate(db); + return; } if (oldVersion < 18) { db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN " diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java index 7d2af9c3..ae8c480b 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java @@ -18,109 +18,111 @@ package com.android.tv.dvr.provider; import android.content.Context; import android.database.Cursor; -import android.os.AsyncTask; import android.support.annotation.Nullable; +import android.util.Log; import com.android.tv.common.concurrent.NamedThreadFactory; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; +import com.android.tv.util.MainThreadExecutor; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */ -public abstract class AsyncDvrDbTask<Params, Progress, Result> - extends AsyncTask<Params, Progress, Result> { +/** {@link DvrDbFuture} that defaults to executing on its own single threaded Executor Service. */ +public abstract class DvrDbFuture<ParamsT, ResultT> { private static final NamedThreadFactory THREAD_FACTORY = - new NamedThreadFactory(AsyncDvrDbTask.class.getSimpleName()); - private static final ExecutorService DB_EXECUTOR = - Executors.newSingleThreadExecutor(THREAD_FACTORY); + new NamedThreadFactory(DvrDbFuture.class.getSimpleName()); + private static final ListeningExecutorService DB_EXECUTOR = + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY)); private static DvrDatabaseHelper sDbHelper; - - private static synchronized DvrDatabaseHelper initializeDbHelper(Context context) { - if (sDbHelper == null) { - sDbHelper = new DvrDatabaseHelper(context.getApplicationContext()); - } - return sDbHelper; - } + private ListenableFuture<ResultT> mFuture; final Context mContext; - private AsyncDvrDbTask(Context context) { + private DvrDbFuture(Context context) { mContext = context; } - /** Execute the task on the {@link #DB_EXECUTOR} thread. */ + /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future*/ @SafeVarargs - public final void executeOnDbThread(Params... params) { - executeOnExecutor(DB_EXECUTOR, params); - } - - @Override - protected final Result doInBackground(Params... params) { - initializeDbHelper(mContext); - return doInDvrBackground(params); + public final ListenableFuture<ResultT> executeOnDbThread( + FutureCallback<ResultT> callback, ParamsT... params) { + if (sDbHelper == null) { + sDbHelper = new DvrDatabaseHelper(mContext.getApplicationContext()); + } + mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params)); + Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance()); + return mFuture; } - /** Executes in the background after {@link #initializeDbHelper(Context)} */ + /** Executes in the background after initializing DbHelper} */ @Nullable - protected abstract Result doInDvrBackground(Params... params); + protected abstract ResultT dbHelperInBackground(ParamsT... params); + + public final boolean isCancelled() { + return mFuture.isCancelled(); + } /** Inserts schedules. */ - public static class AsyncAddScheduleTask - extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { - public AsyncAddScheduleTask(Context context) { + public static class AddScheduleFuture + extends DvrDbFuture<ScheduledRecording, Void> { + public AddScheduleFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(ScheduledRecording... params) { + protected final Void dbHelperInBackground(ScheduledRecording... params) { sDbHelper.insertSchedules(params); return null; } } /** Update schedules. */ - public static class AsyncUpdateScheduleTask - extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { - public AsyncUpdateScheduleTask(Context context) { + public static class UpdateScheduleFuture + extends DvrDbFuture<ScheduledRecording, Void> { + public UpdateScheduleFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(ScheduledRecording... params) { + protected final Void dbHelperInBackground(ScheduledRecording... params) { sDbHelper.updateSchedules(params); return null; } } /** Delete schedules. */ - public static class AsyncDeleteScheduleTask - extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { - public AsyncDeleteScheduleTask(Context context) { + public static class DeleteScheduleFuture + extends DvrDbFuture<ScheduledRecording, Void> { + public DeleteScheduleFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(ScheduledRecording... params) { + protected final Void dbHelperInBackground(ScheduledRecording... params) { sDbHelper.deleteSchedules(params); return null; } } /** Returns all {@link ScheduledRecording}s. */ - public abstract static class AsyncDvrQueryScheduleTask - extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> { - public AsyncDvrQueryScheduleTask(Context context) { + public static class DvrQueryScheduleFuture + extends DvrDbFuture<Void, List<ScheduledRecording>> { + public DvrQueryScheduleFuture(Context context) { super(context); } @Override @Nullable - protected final List<ScheduledRecording> doInDvrBackground(Void... params) { + protected final List<ScheduledRecording> dbHelperInBackground(Void... params) { if (isCancelled()) { return null; } @@ -135,57 +137,59 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } /** Inserts series recordings. */ - public static class AsyncAddSeriesRecordingTask - extends AsyncDvrDbTask<SeriesRecording, Void, Void> { - public AsyncAddSeriesRecordingTask(Context context) { + public static class AddSeriesRecordingFuture + extends DvrDbFuture<SeriesRecording, Void> { + public AddSeriesRecordingFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(SeriesRecording... params) { + protected final Void dbHelperInBackground(SeriesRecording... params) { sDbHelper.insertSeriesRecordings(params); return null; } } /** Update series recordings. */ - public static class AsyncUpdateSeriesRecordingTask - extends AsyncDvrDbTask<SeriesRecording, Void, Void> { - public AsyncUpdateSeriesRecordingTask(Context context) { + public static class UpdateSeriesRecordingFuture + extends DvrDbFuture<SeriesRecording, Void> { + public UpdateSeriesRecordingFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(SeriesRecording... params) { + protected final Void dbHelperInBackground(SeriesRecording... params) { sDbHelper.updateSeriesRecordings(params); return null; } } /** Delete series recordings. */ - public static class AsyncDeleteSeriesRecordingTask - extends AsyncDvrDbTask<SeriesRecording, Void, Void> { - public AsyncDeleteSeriesRecordingTask(Context context) { + public static class DeleteSeriesRecordingFuture + extends DvrDbFuture<SeriesRecording, Void> { + public DeleteSeriesRecordingFuture(Context context) { super(context); } @Override - protected final Void doInDvrBackground(SeriesRecording... params) { + protected final Void dbHelperInBackground(SeriesRecording... params) { sDbHelper.deleteSeriesRecordings(params); return null; } } /** Returns all {@link SeriesRecording}s. */ - public abstract static class AsyncDvrQuerySeriesRecordingTask - extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> { - public AsyncDvrQuerySeriesRecordingTask(Context context) { + public static class DvrQuerySeriesRecordingFuture + extends DvrDbFuture<Void, List<SeriesRecording>> { + private static final String TAG = "DvrQuerySeriesRecording"; + + public DvrQuerySeriesRecordingFuture(Context context) { super(context); } @Override @Nullable - protected final List<SeriesRecording> doInDvrBackground(Void... params) { + protected final List<SeriesRecording> dbHelperInBackground(Void... params) { if (isCancelled()) { return null; } @@ -195,6 +199,8 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> while (c.moveToNext() && !isCancelled()) { scheduledRecordings.add(SeriesRecording.fromCursor(c)); } + } catch (Exception e) { + Log.w(TAG, "Can't query dvr series recording data", e); } return scheduledRecordings; } diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java index 42bc8bcc..7658ca45 100644 --- a/src/com/android/tv/dvr/provider/DvrDbSync.java +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -277,7 +277,6 @@ public class DvrDbSync { } } } else { - long currentTimeMs = System.currentTimeMillis(); ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) .setEndTimeMs(program.getEndTimeUtcMillis()) @@ -361,7 +360,7 @@ public class DvrDbSync { private final long mProgramId; QueryProgramTask(long programId) { - super(mDbExecutor, mContext.getContentResolver(), programId); + super(mDbExecutor, mContext, programId); mProgramId = programId; } diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java index b7d9f3b3..02e197f1 100644 --- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java +++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java @@ -186,7 +186,7 @@ public abstract class EpisodicProgramLoadTask { SqlParams sqlParams = createSqlParams(); return new AsyncProgramQueryTask( TvSingletons.getSingletons(mContext).getDbExecutor(), - mContext.getContentResolver(), + mContext, sqlParams.uri, sqlParams.selection, sqlParams.selectionArgs, @@ -284,7 +284,7 @@ public abstract class EpisodicProgramLoadTask { @Override @WorkerThread - public boolean filter(Cursor c) { + public boolean apply(Cursor c) { if (!mLoadDisallowedProgram && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { return false; @@ -318,10 +318,10 @@ public abstract class EpisodicProgramLoadTask { } @Override - public boolean filter(Cursor c) { + public boolean apply(Cursor c) { return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 - && super.filter(c); + && super.apply(c); } } diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java index 1021b2bc..7d9f7fe2 100644 --- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java @@ -279,7 +279,8 @@ public class InputTaskScheduler { if (schedule.getEndTimeMs() - currentTimeMs <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { Log.e(TAG, "Error! Program ended before recording started:" + schedule); - fail(schedule, + fail( + schedule, ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED); iter.remove(); } @@ -394,19 +395,16 @@ public class InputTaskScheduler { private void fail(ScheduledRecording schedule, int reason) { // It's called when the scheduling has been failed without creating RecordingTask. runOnMainHandler( - new Runnable() { - @Override - public void run() { - ScheduledRecording scheduleInManager = - mDataManager.getScheduledRecording(schedule.getId()); - if (scheduleInManager != null) { - // The schedule should be updated based on the object from DataManager - // in case when it has been updated. - mDataManager.changeState( - scheduleInManager, - ScheduledRecording.STATE_RECORDING_FAILED, - reason); - } + () -> { + ScheduledRecording scheduleInManager = + mDataManager.getScheduledRecording(schedule.getId()); + if (scheduleInManager != null) { + // The schedule should be updated based on the object from DataManager + // in case when it has been updated. + mDataManager.changeState( + scheduleInManager, + ScheduledRecording.STATE_RECORDING_FAILED, + reason); } }); } diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java index 07a29e51..98f668a0 100644 --- a/src/com/android/tv/dvr/recorder/RecordingTask.java +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -17,10 +17,11 @@ package com.android.tv.dvr.recorder; import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; -import android.media.tv.TvRecordingClient.RecordingCallback; import android.net.Uri; import android.os.Build; import android.os.Handler; @@ -36,6 +37,7 @@ import com.android.tv.InputSessionManager.RecordingSession; import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat; import com.android.tv.common.util.Clock; import com.android.tv.common.util.CommonUtils; import com.android.tv.data.api.Channel; @@ -55,7 +57,7 @@ import java.util.concurrent.TimeUnit; */ @WorkerThread @TargetApi(Build.VERSION_CODES.N) -public class RecordingTask extends RecordingCallback +public class RecordingTask extends RecordingCallbackCompat implements Handler.Callback, DvrManager.Listener { private static final String TAG = "RecordingTask"; private static final boolean DEBUG = false; @@ -223,6 +225,14 @@ public class RecordingTask extends RecordingCallback } @Override + public void onRecordingStarted(String inputId, String recUri) { + if (DEBUG) { + Log.d(TAG, "onRecordingStart"); + } + addRecordedProgramId(recUri); + } + + @Override public void onRecordingStopped(Uri recordedProgramUri) { Log.i(TAG, "Recording Stopped: " + mScheduledRecording); Log.i(TAG, "Recording Stopped: stored as " + recordedProgramUri); @@ -340,10 +350,8 @@ public class RecordingTask extends RecordingCallback } private void failAndQuit(Integer reason) { - if (DEBUG) Log.d(TAG, "failAndQuit"); - updateRecordingState( - ScheduledRecording.STATE_RECORDING_FAILED, - reason); + Log.w(TAG, "Recording " + mScheduledRecording + " failed with code " + reason); + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED, reason); mState = State.ERROR; sendRemove(); } @@ -450,6 +458,7 @@ public class RecordingTask extends RecordingCallback private void updateRecordingState(@ScheduledRecording.RecordingState int state) { updateRecordingState(state, null); } + private void updateRecordingState( @ScheduledRecording.RecordingState int state, @Nullable Integer reason) { if (DEBUG) { @@ -471,9 +480,7 @@ public class RecordingTask extends RecordingCallback // has been updated. mScheduledRecording will be updated from // onScheduledRecordingStateChanged. ScheduledRecording.Builder builder = - ScheduledRecording - .buildFrom(schedule) - .setState(state); + ScheduledRecording.buildFrom(schedule).setState(state); if (state == ScheduledRecording.STATE_RECORDING_FAILED && reason != null) { builder.setFailedReason(reason); @@ -484,6 +491,43 @@ public class RecordingTask extends RecordingCallback }); } + private void addRecordedProgramId(String recordedProgramUri) { + if (DEBUG) { + Log.d(TAG, "Adding Recorded Program Id to " + mScheduledRecording); + } + mRecordedProgramUri = Uri.parse(recordedProgramUri); + long id = ContentUris.parseId(mRecordedProgramUri); + mScheduledRecording = + ScheduledRecording.buildFrom(mScheduledRecording).setRecordedProgramId(id).build(); + ContentValues values = new ContentValues(); + values.put( + TvContract.RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + mScheduledRecording.getEndTimeMs() - mScheduledRecording.getStartTimeMs()); + values.put( + TvContract.RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, + mScheduledRecording.getEndTimeMs()); + mContext.getContentResolver().update(mRecordedProgramUri, values, null, null); + runOnMainThread( + new Runnable() { + @Override + public void run() { + ScheduledRecording schedule = + mDataManager.getScheduledRecording(mScheduledRecording.getId()); + if (schedule == null) { + // Schedule has been deleted. Delete the recorded program. + removeRecordedProgram(); + } else { + // Update the state based on the object in DataManager in case when it + // has been updated. mScheduledRecording will be updated from + // onScheduledRecordingStateChanged. + ScheduledRecording.Builder builder = + ScheduledRecording.buildFrom(schedule).setRecordedProgramId(id); + mDataManager.updateScheduledRecording(builder.build()); + } + } + }); + } + @Override public void onStopRecordingRequested(ScheduledRecording recording) { if (recording.getId() != mScheduledRecording.getId()) { @@ -553,7 +597,7 @@ public class RecordingTask extends RecordingCallback @Override public void run() { if (mRecordedProgramUri != null) { - mDvrManager.removeRecordedProgram(mRecordedProgramUri); + mDvrManager.removeRecordedProgram(mRecordedProgramUri, true); } } }); diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java index 4f7a789b..696038cf 100644 --- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java @@ -29,7 +29,6 @@ import android.util.Log; import android.util.LongSparseArray; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.experiments.Experiments; import com.android.tv.common.util.CollectionUtils; import com.android.tv.common.util.SharedPreferencesUtils; import com.android.tv.data.Program; @@ -48,7 +47,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -261,14 +259,11 @@ public class SeriesRecordingScheduler { } private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) { - if (Experiments.CLOUD_EPG.get()) { - FetchSeriesInfoTask task = - new FetchSeriesInfoTask( - seriesRecording, - TvSingletons.getSingletons(mContext).providesEpgReader()); - task.execute(); - mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); - } + FetchSeriesInfoTask task = + new FetchSeriesInfoTask( + seriesRecording, TvSingletons.getSingletons(mContext).providesEpgReader()); + task.execute(); + mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); } /** Pauses the updates of the series recordings. */ @@ -442,21 +437,18 @@ public class SeriesRecordingScheduler { List<Program> programsForEpisode = entry.getValue(); Collections.sort( programsForEpisode, - new Comparator<Program>() { - @Override - public int compare(Program lhs, Program rhs) { - // Place the existing schedule first. - boolean lhsScheduled = isProgramScheduled(dataManager, lhs); - boolean rhsScheduled = isProgramScheduled(dataManager, rhs); - if (lhsScheduled && !rhsScheduled) { - return -1; - } - if (!lhsScheduled && rhsScheduled) { - return 1; - } - // Sort by the start time in ascending order. - return lhs.compareTo(rhs); + (Program lhs, Program rhs) -> { + // Place the existing schedule first. + boolean lhsScheduled = isProgramScheduled(dataManager, lhs); + boolean rhsScheduled = isProgramScheduled(dataManager, rhs); + if (lhsScheduled && !rhsScheduled) { + return -1; + } + if (!lhsScheduled && rhsScheduled) { + return 1; } + // Sort by the start time in ascending order. + return lhs.compareTo(rhs); }); boolean added = false; // Add all the scheduled programs diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java index 32679421..9cd91a64 100644 --- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -27,13 +27,15 @@ import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.android.tv.R; +import com.android.tv.ui.DetailsActivity; + import java.util.Map; /** * TODO: Remove this class once b/32405620 is fixed. This class is for the workaround of b/32405620 * and only for the shared element transition between {@link * com.android.tv.dvr.ui.browse.RecordingCardView} and {@link - * com.android.tv.dvr.ui.browse.DvrDetailsActivity}. + * DetailsActivity}. */ public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index fce94230..5e3caa9c 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -71,7 +71,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.dvr_already_recorded_dialog_title); String description = getString(R.string.dvr_already_recorded_dialog_description); - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 456ad830..a6bbe137 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -78,7 +78,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { getContext(), mDuplicate.getStartTimeMs(), DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE)); - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index 65759555..649cc89a 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -205,7 +205,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { if (description == null) { dismissDialog(); } - Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } @@ -265,7 +265,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { if (description == null) { dismissDialog(); } - Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java deleted file mode 100644 index 677a6cbb..00000000 --- a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2018 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.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; -import android.support.v17.leanback.widget.GuidanceStylist; -import android.support.v17.leanback.widget.GuidedAction; -import com.android.tv.TvSingletons; -import com.android.tv.data.Program; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.util.Utils; -import java.util.List; - -/** - * A fragment which shows the formation of a program. - */ -public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment { - private static final long ACTION_ID_VIEW_SCHEDULE = 1; - private ScheduledRecording mScheduledRecording; - private Program mProgram; - - @Override - public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - long startTime = mProgram.getStartTimeUtcMillis(); - // TODO(b/71717923): use R.string when the strings are finalized - StringBuilder description = new StringBuilder() - .append("This program will start at ") - .append(Utils.getDurationString(getContext(), startTime, startTime, false)); - if (mScheduledRecording != null) { - description.append("\nThis program has been scheduled for recording."); - } - return new GuidanceStylist.Guidance( - mProgram.getTitle(), description.toString(), null, null); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); - mScheduledRecording = - TvSingletons.getSingletons(getContext()) - .getDvrDataManager() - .getScheduledRecordingForProgramId(mProgram.getId()); - actions.add( - new GuidedAction.Builder(activity) - .id(GuidedAction.ACTION_ID_OK) - .title(android.R.string.ok) - .build()); - if (mScheduledRecording != null) { - actions.add( - new GuidedAction.Builder(activity) - .id(ACTION_ID_VIEW_SCHEDULE) - .title("View schedules") - .build()); - } - - } - - @Override - public void onTrackedGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_ID_VIEW_SCHEDULE) { - DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording); - return; - } - dismissDialog(); - } - - @Override - public String getTrackerPrefix() { - return "DvrFutureProgramInfoFragment"; - } -} diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 4a713703..e6b54f67 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -18,17 +18,20 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; +import com.android.tv.ui.DetailsActivity; public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** Key for input ID. Type: String. */ @@ -187,11 +190,27 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } } - /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */ - public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment { + /** A dialog fragment for {@link DvrWriteStoragePermissionRationaleFragment}. */ + public static class DvrWriteStoragePermissionRationaleDialogFragment + extends DvrGuidedStepDialogFragment { @Override - protected DvrGuidedStepFragment onCreateGuidedStepFragment() { - return new DvrFutureProgramInfoFragment(); + protected DvrWriteStoragePermissionRationaleFragment onCreateGuidedStepFragment() { + return new DvrWriteStoragePermissionRationaleFragment(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + Activity activity = getActivity(); + if (activity instanceof DetailsActivity) { + activity.requestPermissions( + new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"}, + DetailsActivity.REQUEST_DELETE); + } else if (activity instanceof DvrSeriesDeletionActivity) { + activity.requestPermissions( + new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"}, + DvrSeriesDeletionActivity.REQUEST_DELETE); + } + super.onDismiss(dialog); } } } diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index e5f40260..02b2da1d 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -25,7 +25,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.util.Log; import com.android.tv.R; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.ui.DetailsActivity; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @@ -65,7 +65,7 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @Override public void onTrackedGuidedActionClicked(GuidedAction action) { Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { + if (activity instanceof DetailsActivity) { activity.finish(); } else { dismissDialog(); diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index 5251e140..72603d03 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -34,7 +34,6 @@ import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; -import com.android.tv.util.Utils; import java.util.Collections; import java.util.List; @@ -104,12 +103,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { mProgram.getEndTimeUtcMillis(), DateUtils.FORMAT_SHOW_TIME)); } else { - description = - Utils.getDurationString( - context, - mProgram.getStartTimeUtcMillis(), - mProgram.getEndTimeUtcMillis(), - true); + description = mProgram.getDurationString(context); } actions.add( new GuidedAction.Builder(context) diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index a2ae1f97..a237f1d2 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -17,16 +17,34 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v17.leanback.app.GuidedStepFragment; +import android.util.Log; +import android.widget.Toast; + import com.android.tv.R; import com.android.tv.Starter; +import com.android.tv.TvSingletons; +import com.android.tv.dvr.DvrManager; + +import java.util.ArrayList; +import java.util.List; /** Activity to show details view in DVR. */ public class DvrSeriesDeletionActivity extends Activity { + private static final String TAG = "DvrSeriesDeletionActivity"; + /** Name of series id added to the Intent. */ public static final String SERIES_RECORDING_ID = "series_recording_id"; + public static final int REQUEST_DELETE = 1; + public static final long INVALID_SERIES_RECORDING_ID = -1; + + private long mSeriesRecordingId = INVALID_SERIES_RECORDING_ID; + private final List<Long> mIdsToDelete = new ArrayList<>(); + @Override public void onCreate(Bundle savedInstanceState) { Starter.start(this); @@ -34,9 +52,61 @@ public class DvrSeriesDeletionActivity extends Activity { setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. if (savedInstanceState == null) { + mSeriesRecordingId = + getIntent().getLongExtra(SERIES_RECORDING_ID, INVALID_SERIES_RECORDING_ID); DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_DELETE: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + deleteSelectedIds(true); + } else { + // NOTE: If Live TV ever supports both embedded and separate DVR inputs + // then we should try to do the delete regardless. + Log.i( + TAG, + "Write permission denied, Not trying to delete the files for series " + + mSeriesRecordingId); + deleteSelectedIds(false); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void deleteSelectedIds(boolean deleteFiles) { + TvSingletons singletons = TvSingletons.getSingletons(this); + int recordingSize = + singletons.getDvrDataManager().getRecordedPrograms(mSeriesRecordingId).size(); + if (!mIdsToDelete.isEmpty()) { + DvrManager dvrManager = singletons.getDvrManager(); + dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles); + } + Toast.makeText( + this, + getResources() + .getQuantityString( + R.plurals.dvr_msg_episodes_deleted, + mIdsToDelete.size(), + mIdsToDelete.size(), + recordingSize), + Toast.LENGTH_LONG) + .show(); + finish(); + } + + void setIdsToDelete(List<Long> ids) { + mIdsToDelete.clear(); + mIdsToDelete.addAll(ids); + } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java index 685f0a58..ff213231 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -29,6 +29,7 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; @@ -53,10 +54,12 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { private static final long ACTION_ID_SELECT_ALL = -111; private static final long ACTION_ID_DELETE = -112; + private DvrManager mDvrManager; private DvrDataManager mDvrDataManager; private DvrWatchedPositionManager mDvrWatchedPositionManager; private List<RecordedProgram> mRecordings; private final Set<Long> mWatchedRecordings = new HashSet<>(); + private final List<Long> mIdsToDelete = new ArrayList<>(); private boolean mAllSelected; private long mSeriesRecordingId; private int mOneLineActionHeight; @@ -67,9 +70,10 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { mSeriesRecordingId = getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); SoftPreconditions.checkArgument(mSeriesRecordingId != -1); - mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); - mDvrWatchedPositionManager = - TvSingletons.getSingletons(context).getDvrWatchedPositionManager(); + TvSingletons singletons = TvSingletons.getSingletons(context); + mDvrManager = singletons.getDvrManager(); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager(); mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); mOneLineActionHeight = getResources() @@ -158,28 +162,7 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_DELETE) { - List<Long> idsToDelete = new ArrayList<>(); - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID - && guidedAction.isChecked()) { - idsToDelete.add(guidedAction.getId()); - } - } - if (!idsToDelete.isEmpty()) { - DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedPrograms(idsToDelete); - } - Toast.makeText( - getContext(), - getResources() - .getQuantityString( - R.plurals.dvr_msg_episodes_deleted, - idsToDelete.size(), - idsToDelete.size(), - mRecordings.size()), - Toast.LENGTH_LONG) - .show(); - finishGuidedStepFragments(); + delete(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { finishGuidedStepFragments(); } else if (actionId == ACTION_ID_SELECT_WATCHED) { @@ -234,6 +217,51 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { }; } + private void delete() { + mIdsToDelete.clear(); + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID + && guidedAction.isChecked()) { + mIdsToDelete.add(guidedAction.getId()); + } + } + ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete); + if (!PermissionUtils.hasWriteExternalStorage(getContext()) + && doesAnySelectedRecordedProgramNeedWritePermission()) { + DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity()); + } else { + deleteSelectedIds(); + } + } + + private boolean doesAnySelectedRecordedProgramNeedWritePermission() { + for (RecordedProgram r : mRecordings) { + if (mIdsToDelete.contains(r.getId()) + && DvrManager.isFile(r.getDataUri()) + && !DvrManager.isFromBundledInput(r)) { + return true; + } + } + return false; + } + + private void deleteSelectedIds() { + if (!mIdsToDelete.isEmpty()) { + mDvrManager.removeRecordedPrograms(mIdsToDelete, true); + } + Toast.makeText( + getContext(), + getResources() + .getQuantityString( + R.plurals.dvr_msg_episodes_deleted, + mIdsToDelete.size(), + mIdsToDelete.size(), + mRecordings.size()), + Toast.LENGTH_LONG) + .show(); + finishGuidedStepFragments(); + } + private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { return getResources() diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index edb62c96..c6e26850 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -101,9 +101,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_series_recording_dialog_title); Drawable icon; if (!mHasConflict) { - icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null); + icon = getResources().getDrawable(R.drawable.quantum_ic_check_circle_white_48, null); } else { - icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); } return new GuidanceStylist.Guidance(title, getDescription(), null, icon); } diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index e93387ab..1ab4c500 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -126,7 +126,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { } else { description = getString(R.string.dvr_stop_recording_dialog_description); } - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java index 16afbdef..a121cf99 100644 --- a/src/com/android/tv/dvr/ui/DvrUiHelper.java +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -37,10 +37,10 @@ import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.widget.ImageView; import android.widget.Toast; + import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvSingletons; -import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.util.CommonUtils; @@ -57,7 +57,6 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialog import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrFutureProgramInfoDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; @@ -65,15 +64,17 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialog import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrWriteStoragePermissionRationaleDialogFragment; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; import com.android.tv.dvr.ui.list.DvrHistoryActivity; import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; +import com.android.tv.ui.DetailsActivity; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -241,13 +242,9 @@ public class DvrUiHelper { } /** Shows program information dialog. */ - public static void showProgramInfoDialog(Activity activity, Program program) { - if (program == null || !BuildConfig.ENG) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true); + public static void showWriteStoragePermissionRationaleDialog(Activity activity) { + showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(), + new Bundle(), false, false); } /** @@ -577,47 +574,43 @@ public class DvrUiHelper { if (dvrItem == null) { return; } - Intent intent = new Intent(activity, DvrDetailsActivity.class); + Intent intent = new Intent(activity, DetailsActivity.class); long recordingId; int viewType; if (dvrItem instanceof ScheduledRecording) { ScheduledRecording schedule = (ScheduledRecording) dvrItem; recordingId = schedule.getId(); if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + viewType = DetailsActivity.CURRENT_RECORDING_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED && schedule.getRecordedProgramId() != null) { recordingId = schedule.getRecordedProgramId(); - viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + viewType = DetailsActivity.RECORDED_PROGRAM_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW; hideViewSchedule = true; - // TODO(b/72638385): pass detailed error message - intent.putExtra( - DvrDetailsActivity.EXTRA_FAILED_MESSAGE, - activity.getString(R.string.dvr_recording_failed)); } else { return; } } else if (dvrItem instanceof RecordedProgram) { recordingId = ((RecordedProgram) dvrItem).getId(); - viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + viewType = DetailsActivity.RECORDED_PROGRAM_VIEW; } else if (dvrItem instanceof SeriesRecording) { recordingId = ((SeriesRecording) dvrItem).getId(); - viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; + viewType = DetailsActivity.SERIES_RECORDING_VIEW; } else { return; } - intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); - intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); + intent.putExtra(DetailsActivity.RECORDING_ID, recordingId); + intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType); + intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); Bundle bundle = null; if (imageView != null) { bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( - activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME) + activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME) .toBundle(); } activity.startActivity(intent, bundle); diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java new file mode 100644 index 00000000..c93f5831 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.dvr.ui; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.R; + +import java.util.List; + +/** + * A fragment which shows the rationale when requesting android.permission.WRITE_EXTERNAL_STORAGE. + */ +public class DvrWriteStoragePermissionRationaleFragment extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + Resources res = getContext().getResources(); + String title = res.getString(R.string.write_storage_permission_rationale_title); + String description = res.getString(R.string.write_storage_permission_rationale_description); + return new GuidanceStylist.Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add( + new GuidedAction.Builder(activity) + .id(GuidedAction.ACTION_ID_OK) + .title(android.R.string.ok) + .build()); + } + + @Override + public void onTrackedGuidedActionClicked(GuidedAction action) { + dismissDialog(); + } + + @Override + public String getTrackerPrefix() { + return "DvrWriteStoragePermissionRationaleFragment"; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java index f3a6fea4..41ace9a4 100644 --- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -27,9 +27,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -// This class is adapted from Leanback's library, which does not support action icon with one-line -// label. This class modified its getPresenter method to support the above situation. -class ActionPresenterSelector extends PresenterSelector { +/** + * This class is adapted from Leanback's library, which does not support action icon with one-line + * label. This class modified its getPresenter method to support the above situation. + */ +public class ActionPresenterSelector extends PresenterSelector { private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); private final Presenter[] mPresenters = diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java index 7e7e1f75..8c311d68 100644 --- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -18,23 +18,34 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.content.res.Resources; +import android.media.tv.TvInputManager; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; /** {@link RecordingDetailsFragment} for current recording in DVR. */ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { private static final int ACTION_STOP_RECORDING = 1; + private static final int ACTION_RESUME_PLAYING = 2; + private static final int ACTION_PLAY_FROM_BEGINNING = 3; private DvrDataManager mDvrDataManger; + private RecordedProgram mRecordedProgram; + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + private boolean mPaused; private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = new DvrDataManager.ScheduledRecordingListener() { @Override @@ -68,10 +79,32 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { super.onAttach(context); mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager(); mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); + mDvrWatchedPositionManager = + TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager(); + mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context); + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateActions(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; } @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { + Long recordedProgramId = getRecording().getRecordedProgramId(); + if (recordedProgramId != null) { + mRecordedProgram = mDvrDataManger.getRecordedProgram(recordedProgramId); + } SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); @@ -82,6 +115,35 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { res.getString(R.string.dvr_detail_stop_recording), null, res.getDrawable(R.drawable.lb_ic_stop))); + if (mConcurrentDvrPlaybackFlags.enabled() + && mRecordedProgram != null + && mRecordedProgram.isPartial()) { + if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + adapter.set( + ACTION_RESUME_PLAYING, + new Action( + ACTION_RESUME_PLAYING, + res.getString(R.string.dvr_detail_resume_play), + null, + res.getDrawable(R.drawable.lb_ic_play))); + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_play_from_beginning), + null, + res.getDrawable(R.drawable.lb_ic_replay))); + } else { + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_watch), + null, + res.getDrawable(R.drawable.lb_ic_play))); + } + } return adapter; } @@ -107,6 +169,13 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { } } }); + } else if (action.getId() == ACTION_RESUME_PLAYING) { + startPlayback( + mRecordedProgram, + mDvrWatchedPositionManager.getWatchedPosition( + mRecordedProgram.getId())); + } else if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); } } }; diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java index cba6293b..e179743c 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.data.Program; import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; @@ -29,7 +30,7 @@ import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; /** A class for details content. */ -class DetailsContent { +public class DetailsContent { /** Constant for invalid time. */ public static final long INVALID_TIME = -1; @@ -40,6 +41,7 @@ class DetailsContent { private String mLogoImageUri; private String mBackgroundImageUri; private boolean mUsingChannelLogo; + private boolean mShowErrorMessage; static DetailsContent createFromRecordedProgram( Context context, RecordedProgram recordedProgram) { @@ -59,6 +61,23 @@ class DetailsContent { .build(context); } + public static DetailsContent createFromProgram(Context context, Program program) { + return new DetailsContent.Builder() + .setChannelId(program.getChannelId()) + .setProgramTitle(program.getTitle()) + .setSeasonNumber(program.getSeasonNumber()) + .setEpisodeNumber(program.getEpisodeNumber()) + .setStartTimeUtcMillis(program.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(program.getEndTimeUtcMillis()) + .setDescription( + TextUtils.isEmpty(program.getLongDescription()) + ? program.getDescription() + : program.getLongDescription()) + .setPosterArtUri(program.getPosterArtUri()) + .setThumbnailUri(program.getThumbnailUri()) + .build(context); + } + static DetailsContent createFromSeriesRecording( Context context, SeriesRecording seriesRecording) { return new DetailsContent.Builder() @@ -79,37 +98,9 @@ class DetailsContent { TvSingletons.getSingletons(context) .getChannelDataManager() .getChannel(scheduledRecording.getChannelId()); - String description = - !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) - ? scheduledRecording.getProgramDescription() - : scheduledRecording.getProgramLongDescription(); - if (TextUtils.isEmpty(description)) { - description = channel != null ? channel.getDescription() : null; - } - return new DetailsContent.Builder() - .setChannelId(scheduledRecording.getChannelId()) - .setProgramTitle(scheduledRecording.getProgramTitle()) - .setSeasonNumber(scheduledRecording.getSeasonNumber()) - .setEpisodeNumber(scheduledRecording.getEpisodeNumber()) - .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs()) - .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs()) - .setDescription(description) - .setPosterArtUri(scheduledRecording.getProgramPosterArtUri()) - .setThumbnailUri(scheduledRecording.getProgramThumbnailUri()) - .build(context); - } - - static DetailsContent createFromFailedScheduledRecording( - Context context, ScheduledRecording scheduledRecording, String errMsg) { - Channel channel = - TvSingletons.getSingletons(context) - .getChannelDataManager() - .getChannel(scheduledRecording.getChannelId()); String description; - if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED - && errMsg != null) { - description = errMsg - + " (Error code: " + scheduledRecording.getFailedReason() + ")"; + if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + description = getErrorMessage(context, scheduledRecording); } else { description = !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) @@ -129,9 +120,39 @@ class DetailsContent { .setDescription(description) .setPosterArtUri(scheduledRecording.getProgramPosterArtUri()) .setThumbnailUri(scheduledRecording.getProgramThumbnailUri()) + .setShowErrorMessage( + scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) .build(context); } + private static String getErrorMessage(Context context, ScheduledRecording recording) { + int reason = recording.getFailedReason() == null + ? ScheduledRecording.FAILED_REASON_OTHER + : recording.getFailedReason(); + switch (reason) { + case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return context.getString(R.string.dvr_recording_failed_not_started); + case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY: + return context.getString(R.string.dvr_recording_failed_resource_busy); + case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE: + return context.getString( + R.string.dvr_recording_failed_input_unavailable, + recording.getInputId()); + case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported); + case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE: + return context.getString(R.string.dvr_recording_failed_insufficient_space); + case ScheduledRecording.FAILED_REASON_OTHER: // fall through + case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through + case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through + case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through + case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through + case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through + default: + return context.getString(R.string.dvr_recording_failed_system_failure, reason); + } + } + private DetailsContent() {} /** Returns title. */ @@ -169,6 +190,11 @@ class DetailsContent { return mUsingChannelLogo; } + /** Returns if the error message should be shown. */ + public boolean shouldShowErrorMessage() { + return mShowErrorMessage; + } + /** Copies other details content. */ public void copyFrom(DetailsContent other) { if (this == other) { @@ -181,6 +207,7 @@ class DetailsContent { mLogoImageUri = other.mLogoImageUri; mBackgroundImageUri = other.mBackgroundImageUri; mUsingChannelLogo = other.mUsingChannelLogo; + mShowErrorMessage = other.mShowErrorMessage; } /** A class for building details content. */ @@ -266,6 +293,11 @@ class DetailsContent { return this; } + private Builder setShowErrorMessage(boolean showErrorMessage) { + mDetailsContent.mShowErrorMessage = showErrorMessage; + return this; + } + private void createStyledTitle(Context context, Channel channel) { CharSequence title = DvrUiHelper.getStyledTitleWithEpisodeNumber( diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java index aec8c411..6b5fd1fd 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -45,12 +45,13 @@ import com.android.tv.util.Utils; * The latter class are re-used to provide a customized version of {@link * android.support.v17.leanback.widget.DetailsOverviewRow}. */ -class DetailsContentPresenter extends Presenter { +public class DetailsContentPresenter extends Presenter { /** The ViewHolder for the {@link DetailsContentPresenter}. */ public static class ViewHolder extends Presenter.ViewHolder { final TextView mTitle; final TextView mSubtitle; final LinearLayout mDescriptionContainer; + final LinearLayout mErrorMessage; final TextView mBody; final TextView mReadMoreView; final int mTitleMargin; @@ -150,6 +151,8 @@ class DetailsContentPresenter extends Presenter { }); mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); + mErrorMessage = + (LinearLayout) view.findViewById(R.id.dvr_details_description_error_message); mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); mDescriptionContainer = (LinearLayout) view.findViewById(R.id.dvr_details_description_container); @@ -321,6 +324,9 @@ class DetailsContentPresenter extends Presenter { if (TextUtils.isEmpty(detailsContent.getDescription())) { vh.mBody.setVisibility(View.GONE); } else { + if (detailsContent.shouldShowErrorMessage()) { + vh.mErrorMessage.setVisibility(View.VISIBLE); + } vh.mBody.setText(detailsContent.getDescription()); vh.mBody.setVisibility(View.VISIBLE); vh.mBody.setLineSpacing( diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java index 849360b8..4e41daee 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -24,7 +24,7 @@ import android.os.Handler; import android.support.v17.leanback.app.BackgroundManager; /** The Background Helper. */ -class DetailsViewBackgroundHelper { +public class DetailsViewBackgroundHelper { // Background delay serves to avoid kicking off expensive bitmap loading // in case multiple backgrounds are set in quick succession. private static final int SET_BACKGROUND_DELAY_MS = 100; diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java index 6cc1c7a1..5743ea5c 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -22,9 +22,15 @@ import android.media.tv.TvInputManager; import android.os.Bundle; import com.android.tv.R; import com.android.tv.Starter; +import com.android.tv.perf.PerformanceMonitorManagerFactory; /** {@link android.app.Activity} for DVR UI. */ public class DvrBrowseActivity extends Activity { + + { + PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit(); + } + private DvrBrowseFragment mFragment; @Override diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java index 40b3a1f0..17ba1939 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -31,9 +31,7 @@ import android.support.v17.leanback.widget.TitleViewAdapter; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; - import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.data.GenreItems; import com.android.tv.dvr.DvrDataManager; @@ -47,7 +45,7 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.SortedArrayAdapter; - +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -66,7 +64,7 @@ public class DvrBrowseFragment extends BrowseFragment private static final String TAG = "DvrBrowseFragment"; private static final boolean DEBUG = false; - private static final int MAX_RECENT_ITEM_COUNT = 10; + private static final int MAX_RECENT_ITEM_COUNT = 4; private static final int MAX_SCHEDULED_ITEM_COUNT = 4; private boolean mShouldShowScheduleRow; @@ -104,93 +102,84 @@ public class DvrBrowseFragment extends BrowseFragment }; private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof SeriesRecording) { - lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); - } - if (rhs instanceof SeriesRecording) { - rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); - } - if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else { - return -1; - } - } else if (rhs instanceof RecordedProgram) { - return 1; + (Object lhs, Object rhs) -> { + if (lhs instanceof SeriesRecording) { + lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); + } + if (rhs instanceof SeriesRecording) { + rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); + } + if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); } else { - return 0; + return -1; } + } else if (rhs instanceof RecordedProgram) { + return 1; + } else { + return 0; } }; private static final Comparator<Object> SCHEDULE_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else { - return -1; - } - } else if (rhs instanceof ScheduledRecording) { - return 1; + (Object lhs, Object rhs) -> { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); } else { - return 0; + return -1; } + } else if (rhs instanceof ScheduledRecording) { + return 1; + } else { + return 0; } }; static final Comparator<Object> RECENT_ROW_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .reversed() - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else if (rhs instanceof RecordedProgram) { - ScheduledRecording scheduled = (ScheduledRecording) lhs; - RecordedProgram recorded = (RecordedProgram) rhs; - int compare = - Long.compare( - recorded.getStartTimeUtcMillis(), - scheduled.getStartTimeMs()); - // recorded program first when the start times are the same - return compare == 0 ? 1 : compare; - } else { - return -1; - } - } else if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else if (rhs instanceof ScheduledRecording) { - RecordedProgram recorded = (RecordedProgram) lhs; - ScheduledRecording scheduled = (ScheduledRecording) rhs; - int compare = - Long.compare( - scheduled.getStartTimeMs(), - recorded.getStartTimeUtcMillis()); - // recorded program first when the start times are the same - return compare == 0 ? -1 : compare; - } else { - return -1; - } + (Object lhs, Object rhs) -> { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .reversed() + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else if (rhs instanceof RecordedProgram) { + ScheduledRecording scheduled = (ScheduledRecording) lhs; + RecordedProgram recorded = (RecordedProgram) rhs; + int compare = + Long.compare( + recorded.getStartTimeUtcMillis(), + scheduled.getStartTimeMs()); + // recorded program first when the start times are the same + return compare == 0 ? 1 : compare; + } else { + return -1; + } + } else if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else if (rhs instanceof ScheduledRecording) { + RecordedProgram recorded = (RecordedProgram) lhs; + ScheduledRecording scheduled = (ScheduledRecording) rhs; + int compare = + Long.compare( + scheduled.getStartTimeMs(), + recorded.getStartTimeUtcMillis()); + // recorded program first when the start times are the same + return compare == 0 ? -1 : compare; } else { - return !(rhs instanceof RecordedProgram) - && !(rhs instanceof ScheduledRecording) - ? 0 : 1; + return -1; } + } else { + return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording) + ? 0 + : 1; } }; @@ -207,13 +196,7 @@ public class DvrBrowseFragment extends BrowseFragment } }; - private final Runnable mUpdateRowsRunnable = - new Runnable() { - @Override - public void run() { - updateRows(); - } - }; + private final Runnable mUpdateRowsRunnable = this::updateRows; @Override public void onCreate(Bundle savedInstanceState) { @@ -233,13 +216,10 @@ public class DvrBrowseFragment extends BrowseFragment SeriesRecording.class, new SeriesRecordingPresenter(context)) .addClassPresenter( FullScheduleCardHolder.class, - new FullSchedulesCardPresenter(context)); + new FullSchedulesCardPresenter(context)) + .addClassPresenter( + DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context)); - if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) { - mPresenterSelector.addClassPresenter( - DvrHistoryCardHolder.class, - new DvrHistoryCardPresenter(context)); - } mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); prepareUiElements(); @@ -310,7 +290,9 @@ public class DvrBrowseFragment extends BrowseFragment @Override public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramChanged(recordedProgram); + if (recordedProgram.isVisible()) { + handleRecordedProgramChanged(recordedProgram); + } } postUpdateRows(); } @@ -340,6 +322,9 @@ public class DvrBrowseFragment extends BrowseFragment public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { for (ScheduledRecording scheduleRecording : scheduledRecordings) { mScheduleAdapter.remove(scheduleRecording); + if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + mRecentAdapter.remove(scheduleRecording); + } } } @@ -351,6 +336,9 @@ public class DvrBrowseFragment extends BrowseFragment } else { mScheduleAdapter.removeWithId(scheduleRecording); } + if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + mRecentAdapter.change(scheduleRecording); + } } } @@ -443,16 +431,17 @@ public class DvrBrowseFragment extends BrowseFragment mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); // Recorded Programs. for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) { - // only get failed recordings - for (ScheduledRecording scheduledRecording - : mDvrDataManager.getFailedScheduledRecordings()) { - onScheduledRecordingAdded(scheduledRecording); + if (recordedProgram.isVisible()) { + handleRecordedProgramAdded(recordedProgram, false); } - mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER); } + // only get failed recordings + for (ScheduledRecording scheduledRecording : + mDvrDataManager.getFailedScheduledRecordings()) { + onScheduledRecordingAdded(scheduledRecording); + } + mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER); + // Series Recordings. Series recordings should be added after recorded programs, because // we build series recordings' latest program information while adding recorded // programs. @@ -592,9 +581,9 @@ public class DvrBrowseFragment extends BrowseFragment } } - private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) { + private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) { List<RecordedProgramAdapter> result = new ArrayList<>(); - if (genres == null || genres.length == 0) { + if (genres == null || genres.isEmpty()) { result.add(mGenreAdapters[mGenreAdapters.length - 1]); } else { for (String genre : genres) { @@ -642,8 +631,8 @@ public class DvrBrowseFragment extends BrowseFragment private void updateRows() { int visibleRowsCount = 1; // Schedule's Row will never be empty - int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0; - if (mRecentAdapter.size() <= recentRowMinSize) { + if (mRecentAdapter.size() <= 1) { + // remove the row if there is only the DVR history card mRowsAdapter.remove(mRecentRow); } else { if (mRowsAdapter.indexOf(mRecentRow) < 0) { @@ -673,6 +662,9 @@ public class DvrBrowseFragment extends BrowseFragment } } } + if (getSelectedPosition() >= mRowsAdapter.size()) { + setSelectedPosition(mRecentAdapter.size() - 1); + } } private boolean needToShowScheduledRecording(ScheduledRecording recording) { @@ -713,16 +705,13 @@ public class DvrBrowseFragment extends BrowseFragment SeriesAdapter() { super( mPresenterSelector, - new Comparator<SeriesRecording>() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - if (lhs.isStopped() && !rhs.isStopped()) { - return 1; - } else if (!lhs.isStopped() && rhs.isStopped()) { - return -1; - } - return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + (SeriesRecording lhs, SeriesRecording rhs) -> { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); }); } diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java deleted file mode 100644 index 0336b319..00000000 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2016 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.dvr.ui.browse; - -import android.app.Activity; -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.transition.Transition; -import android.transition.Transition.TransitionListener; -import android.view.View; -import com.android.tv.R; -import com.android.tv.Starter; -import com.android.tv.dialog.PinDialogFragment; - -/** Activity to show details view in DVR. */ -public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener { - /** Name of record id added to the Intent. */ - public static final String RECORDING_ID = "record_id"; - - /** - * Name of flag added to the Intent to determine if details view should hide "View schedule" - * button. - */ - public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; - - /** Name of details view's type added to the intent. */ - public static final String DETAILS_VIEW_TYPE = "details_view_type"; - - /** Name of shared element between activities. */ - public static final String SHARED_ELEMENT_NAME = "shared_element"; - - /** Name of error message of a failed recording */ - public static final String EXTRA_FAILED_MESSAGE = "failed_message"; - - /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */ - public static final int CURRENT_RECORDING_VIEW = 1; - - /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */ - public static final int SCHEDULED_RECORDING_VIEW = 2; - - /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */ - public static final int RECORDED_PROGRAM_VIEW = 3; - - /** SERIES_RECORDING_VIEW refers to series recording in DVR. */ - public static final int SERIES_RECORDING_VIEW = 4; - - private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener; - - @Override - public void onCreate(Bundle savedInstanceState) { - Starter.start(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_dvr_details); - long recordId = getIntent().getLongExtra(RECORDING_ID, -1); - int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); - boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); - String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE); - if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(RECORDING_ID, recordId); - DetailsFragment detailsFragment = null; - if (detailsViewType == CURRENT_RECORDING_VIEW) { - detailsFragment = new CurrentRecordingDetailsFragment(); - } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { - args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); - args.putString(EXTRA_FAILED_MESSAGE, failedMsg); - detailsFragment = new ScheduledRecordingDetailsFragment(); - } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { - detailsFragment = new RecordedProgramDetailsFragment(); - } else if (detailsViewType == SERIES_RECORDING_VIEW) { - detailsFragment = new SeriesRecordingDetailsFragment(); - } - detailsFragment.setArguments(args); - getFragmentManager() - .beginTransaction() - .replace(R.id.dvr_details_view_frame, detailsFragment) - .commit(); - } - - // This is a workaround for the focus on O device - addTransitionListener(); - } - - @Override - public void onPinChecked(boolean checked, int type, String rating) { - if (mOnPinCheckedListener != null) { - mOnPinCheckedListener.onPinChecked(checked, type, rating); - } - } - - void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) { - mOnPinCheckedListener = listener; - } - - private void addTransitionListener() { - getWindow() - .getSharedElementEnterTransition() - .addListener( - new TransitionListener() { - @Override - public void onTransitionStart(Transition transition) { - // Do nothing - } - - @Override - public void onTransitionEnd(Transition transition) { - View actions = findViewById(R.id.details_overview_actions); - if (actions != null) { - actions.requestFocus(); - } - } - - @Override - public void onTransitionCancel(Transition transition) { - // Do nothing - - } - - @Override - public void onTransitionPause(Transition transition) { - // Do nothing - } - - @Override - public void onTransitionResume(Transition transition) { - // Do nothing - } - }); - } -} diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java index 8f4e4dab..f90981f0 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -47,8 +47,10 @@ import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.parental.ParentalControlSettings; +import com.android.tv.ui.DetailsActivity; import com.android.tv.util.ToastUtils; import com.android.tv.util.images.ImageLoader; +import com.google.common.collect.ImmutableList; import java.io.File; abstract class DvrDetailsFragment extends DetailsFragment { @@ -89,7 +91,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { rowPresenter.setBackgroundColor( getResources().getColor(R.color.common_tv_background, null)); rowPresenter.setSharedElementEnterTransition( - getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME); + getActivity(), DetailsActivity.SHARED_ELEMENT_NAME); rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); setAdapter(mRowsAdapter); @@ -221,7 +223,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { checkPinToPlay(recordedProgram, seekTimeMs); return; } - TvContentRating[] ratings = recordedProgram.getContentRatings(); + ImmutableList<TvContentRating> ratings = recordedProgram.getContentRatings(); TvContentRating blockRatings = parental.getBlockedRating(ratings); if (blockRatings != null) { checkPinToPlay(recordedProgram, seekTimeMs); @@ -245,15 +247,14 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { - SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity); - if (getActivity() instanceof DvrDetailsActivity) { - ((DvrDetailsActivity) getActivity()) + SoftPreconditions.checkState(getActivity() instanceof DetailsActivity); + if (getActivity() instanceof DetailsActivity) { + ((DetailsActivity) getActivity()) .setOnPinCheckListener( new OnPinCheckedListener() { @Override public void onPinChecked(boolean checked, int type, String rating) { - ((DvrDetailsActivity) getActivity()) - .setOnPinCheckListener(null); + ((DetailsActivity) getActivity()).setOnPinCheckListener(null); if (checked && type == PinDialogFragment diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java index 47b1a198..bf963547 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -24,10 +24,13 @@ import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.ui.DetailsActivity; /** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment @@ -80,7 +83,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment @Override protected boolean onLoadRecordingDetails(Bundle args) { - long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long recordedProgramId = args.getLong(DetailsActivity.RECORDING_ID); mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); return mRecordedProgram != null; } @@ -138,15 +141,24 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment mDvrWatchedPositionManager.getWatchedPosition( mRecordedProgram.getId())); } else if (action.getId() == ACTION_DELETE_RECORDING) { - DvrManager dvrManager = - TvSingletons.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedProgram(mRecordedProgram); - getActivity().finish(); + delete(); } } }; } + private void delete() { + if (!PermissionUtils.hasWriteExternalStorage(getContext()) + && DvrManager.isFile(mRecordedProgram.getDataUri()) + && !DvrManager.isFromBundledInput(mRecordedProgram)) { + DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity()); + } else { + DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedProgram(mRecordedProgram, true); + getActivity().finish(); + } + } + @Override public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java index fe3c52d9..c83ceaf0 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -48,11 +48,10 @@ public class RecordingCardView extends BaseCardView { private final int mImageWidth; private final int mImageHeight; private String mImageUri; + private final ImageView mContentIconView; private final TextView mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; - private final View mAffiliatedIconContainer; - private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; private final FrameLayout mTitleArea; private final TextView mFoldedTitleView; @@ -94,8 +93,7 @@ public class RecordingCardView extends BaseCardView { mImageWidth = imageWidth; mImageHeight = imageHeight; mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); - mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); - mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); + mContentIconView = (ImageView) findViewById(R.id.content_icon); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); mTitleArea = (FrameLayout) findViewById(R.id.title_area); @@ -184,6 +182,7 @@ public class RecordingCardView extends BaseCardView { } void setContent(CharSequence majorContent, CharSequence minorContent) { + mContentIconView.setVisibility(View.GONE); if (!TextUtils.isEmpty(majorContent)) { mMajorContentView.setText(majorContent); mMajorContentView.setVisibility(View.VISIBLE); @@ -198,6 +197,24 @@ public class RecordingCardView extends BaseCardView { } } + void setRecordingFailedContent(Context context) { + mContentIconView.setVisibility(View.VISIBLE); + mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp); + mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period)); + mMajorContentView.setVisibility(View.VISIBLE); + mMajorContentView.setTextColor( + getResources().getColor(R.color.dvr_recording_failed_text_color, null)); + } + + void setRecordingConflictContent(Context context) { + mContentIconView.setVisibility(View.VISIBLE); + mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp); + mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict)); + mMajorContentView.setVisibility(View.VISIBLE); + mMajorContentView.setTextColor( + getResources().getColor(R.color.dvr_recording_conflict_text_color, null)); + } + /** Sets progress bar. If progress is {@code null}, hides progress bar. */ void setProgressBar(Integer progress) { if (progress == null) { @@ -245,19 +262,6 @@ public class RecordingCardView extends BaseCardView { } /** - * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner - * of the poster. - */ - public void setAffiliatedIcon(int imageResId) { - if (imageResId > 0) { - mAffiliatedIconContainer.setVisibility(View.VISIBLE); - mAffiliatedIcon.setImageResource(imageResId); - } else { - mAffiliatedIconContainer.setVisibility(View.INVISIBLE); - } - } - - /** * Sets the background image URI of the card view, which will be displayed as background when * the view is clicked and shows its details fragment. */ diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java index aa2ccf75..243681c6 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import com.android.tv.TvSingletons; import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.ui.DetailsActivity; /** {@link DetailsFragment} for recordings in DVR. */ abstract class RecordingDetailsFragment extends DvrDetailsFragment { @@ -33,7 +34,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment { @Override protected boolean onLoadRecordingDetails(Bundle args) { - long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long scheduledRecordingId = args.getLong(DetailsActivity.RECORDING_ID); mRecording = TvSingletons.getSingletons(getContext()) .getDvrDataManager() diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java index 302b8318..f08bb12b 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -21,10 +21,12 @@ import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; + import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.ui.DetailsActivity; /** {@link RecordingDetailsFragment} for scheduled recording in DVR. */ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { @@ -34,14 +36,12 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment private DvrManager mDvrManager; private Action mScheduleAction; private boolean mHideViewSchedule; - private String mFailedMessage; @Override public void onCreate(Bundle savedInstance) { Bundle args = getArguments(); mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); - mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); - mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE); + mHideViewSchedule = args.getBoolean(DetailsActivity.HIDE_VIEW_SCHEDULE); super.onCreate(savedInstance); } @@ -54,17 +54,6 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment } @Override - protected void onCreateInternal() { - if (mFailedMessage == null) { - super.onCreateInternal(); - return; - } - setDetailsOverviewRow( - DetailsContent.createFromFailedScheduledRecording( - getContext(), getScheduledRecording(), mFailedMessage)); - } - - @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java index 8e028689..3d279354 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -119,21 +119,17 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording); cardView.setTitle(details.getTitle()); cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); - if (mDvrManager.isConflicting(recording)) { - cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); - } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp); + if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + cardView.setRecordingFailedContent(mContext); + } else if (mDvrManager.isConflicting(recording)) { + cardView.setRecordingConflictContent(mContext); } else { - cardView.setAffiliatedIcon(0); + cardView.setContent(generateMajorContent(recording), null); } - cardView.setContent(generateMajorContent(recording), null); cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); } private String generateMajorContent(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - return mContext.getString(R.string.dvr_recording_failed); - } int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs()); if (dateDifference <= 0) { diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java index 2cd191a7..9104ef10 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -20,6 +20,7 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.media.tv.TvInputManager; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v17.leanback.app.DetailsFragment; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; @@ -41,6 +42,7 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.ui.DetailsActivity; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -135,7 +137,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment @Override protected boolean onLoadRecordingDetails(Bundle args) { - long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long recordId = args.getLong(DetailsActivity.RECORDING_ID); mSeries = TvSingletons.getSingletons(getActivity()) .getDvrDataManager() @@ -215,6 +217,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment } /** The programs are sorted by season number and episode number. */ + @Nullable private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { for (int i = programs.size() - 1; i >= 0; i--) { RecordedProgram program = programs.get(i); @@ -289,7 +292,8 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment } } } - if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + if (mRecommendRecordedProgram != null + && recordedProgram.getId() == mRecommendRecordedProgram.getId()) { updateWatchAction(); } } @@ -339,14 +343,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment new ListRow( header, new SeasonRowAdapter( - selector, - new Comparator<RecordedProgram>() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); - } - }, - seasonNumber)); + selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber)); getRowsAdapter().add(position, row); return row; } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 38d3d582..11680a0d 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -37,7 +37,6 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.api.Channel; @@ -90,18 +89,19 @@ class ScheduleRowPresenter extends RowPresenter { private ScheduleRowPresenter mPresenter; @ScheduleRowAction private int[] mActions; private boolean mLtr; - private LinearLayout mInfoContainer; + private final LinearLayout mInfoContainer; // The first action is on the right of the second action. - private RelativeLayout mSecondActionContainer; - private RelativeLayout mFirstActionContainer; - private View mSelectorView; - private TextView mTimeView; - private TextView mProgramTitleView; - private TextView mInfoSeparatorView; - private TextView mChannelNameView; - private TextView mConflictInfoView; - private ImageView mSecondActionView; - private ImageView mFirstActionView; + private final RelativeLayout mSecondActionContainer; + private final RelativeLayout mFirstActionContainer; + private final View mSelectorView; + private final TextView mTimeView; + private final TextView mProgramTitleView; + private final TextView mInfoSeparatorView; + private final TextView mChannelNameView; + private final ImageView mExtraInfoIcon; + private final TextView mExtraInfoView; + private final ImageView mSecondActionView; + private final ImageView mFirstActionView; private Runnable mPendingAnimationRunnable; @@ -117,14 +117,11 @@ class ScheduleRowPresenter extends RowPresenter { @Override public void onFocusChange(View view, boolean focused) { view.post( - new Runnable() { - @Override - public void run() { - if (view.isFocused()) { - mPresenter.mLastFocusedViewId = view.getId(); - } - updateSelector(); + () -> { + if (view.isFocused()) { + mPresenter.mLastFocusedViewId = view.getId(); } + updateSelector(); }); } }; @@ -146,7 +143,8 @@ class ScheduleRowPresenter extends RowPresenter { mProgramTitleView = (TextView) view.findViewById(R.id.program_title); mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator); mChannelNameView = (TextView) view.findViewById(R.id.channel_name); - mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info); + mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon); + mExtraInfoView = (TextView) view.findViewById(R.id.extra_info); Resources res = view.getResources(); mSelectorTranslationDelta = res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) @@ -311,7 +309,7 @@ class ScheduleRowPresenter extends RowPresenter { mInfoContainer .getResources() .getColor(R.color.dvr_schedules_item_info_grey, null)); - mConflictInfoView.setTextColor( + mExtraInfoView.setTextColor( mInfoContainer .getResources() .getColor(R.color.dvr_schedules_item_info_grey, null)); @@ -327,7 +325,7 @@ class ScheduleRowPresenter extends RowPresenter { mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); mChannelNameView.setTextColor( mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); - mConflictInfoView.setTextColor( + mExtraInfoView.setTextColor( mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); } } @@ -426,39 +424,76 @@ class ScheduleRowPresenter extends RowPresenter { } } ScheduledRecording schedule = row.getSchedule(); - if (mDvrManager.isConflicting(schedule) - || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) - && schedule != null - && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) { - String conflictInfo; - if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) - && schedule != null - && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - // TODO(b/72638385): show real error messages - // TODO(b/72638385): use a better name for ConflictInfoXXX - conflictInfo = "Failed"; - if (schedule.getFailedReason() != null) { - conflictInfo += " (Error code: " + schedule.getFailedReason() + ")"; - } + viewHolder.mExtraInfoIcon.setVisibility(View.GONE); + if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) { + String extraInfo; + if (isFailedRecording(schedule)) { + extraInfo = + mContext.getString(R.string.dvr_recording_failed_short) + + " " + + getErrorMessage(schedule); + viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE); } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { - conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; + extraInfo = mTunerConflictWillBePartiallyRecordedInfo; } else { - conflictInfo = mTunerConflictWillNotBeRecordedInfo; + extraInfo = mTunerConflictWillNotBeRecordedInfo; } - viewHolder.mConflictInfoView.setText(conflictInfo); - viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); + viewHolder.mExtraInfoView.setText(extraInfo); + viewHolder.mExtraInfoView.setVisibility(View.VISIBLE); } else { - viewHolder.mConflictInfoView.setVisibility(View.GONE); + viewHolder.mExtraInfoView.setVisibility(View.GONE); } if (shouldBeGrayedOut(row)) { viewHolder.greyOutInfo(); } else { viewHolder.whiteBackInfo(); } + if (isFailedRecording(schedule)) { + viewHolder.mExtraInfoView.setTextColor( + viewHolder + .mInfoContainer + .getResources() + .getColor(R.color.dvr_recording_failed_text_color, null)); + } viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } + private boolean isFailedRecording(ScheduledRecording scheduledRecording) { + return scheduledRecording != null + && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED; + } + + private String getErrorMessage(ScheduledRecording recording) { + int reason = + recording.getFailedReason() == null + ? ScheduledRecording.FAILED_REASON_OTHER + : recording.getFailedReason(); + switch (reason) { + case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return mContext.getString(R.string.dvr_recording_failed_not_started_short); + case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY: + return mContext.getString(R.string.dvr_recording_failed_resource_busy_short); + case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE: + return mContext.getString( + R.string.dvr_recording_failed_input_unavailable_short, + recording.getInputId()); + case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return mContext.getString( + R.string.dvr_recording_failed_input_dvr_unsupported_short); + case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE: + return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short); + case ScheduledRecording.FAILED_REASON_OTHER: // fall through + case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through + case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through + case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through + case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through + case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through + default: + return mContext.getString(R.string.dvr_recording_failed_system_failure, reason); + } + } + private int getImageForAction(@ScheduleRowAction int action) { switch (action) { case ACTION_START_RECORDING: @@ -512,7 +547,8 @@ class ScheduleRowPresenter extends RowPresenter { return schedule != null && (schedule.isNotStarted() || schedule.isInProgress() - || schedule.isFinished()); + || schedule.isFinished() + || schedule.isFailed()); } /** Called when the button in a row is clicked. */ @@ -702,23 +738,17 @@ class ScheduleRowPresenter extends RowPresenter { prepareShowActionView(viewHolder.mSecondActionContainer); prepareShowActionView(viewHolder.mFirstActionContainer); viewHolder.mPendingAnimationRunnable = - new Runnable() { - @Override - public void run() { - showActionView(viewHolder.mSecondActionContainer); - showActionView(viewHolder.mFirstActionContainer); - } + () -> { + showActionView(viewHolder.mSecondActionContainer); + showActionView(viewHolder.mFirstActionContainer); }; break; case 1: prepareShowActionView(viewHolder.mFirstActionContainer); viewHolder.mPendingAnimationRunnable = - new Runnable() { - @Override - public void run() { - hideActionView(viewHolder.mSecondActionContainer, View.GONE); - showActionView(viewHolder.mFirstActionContainer); - } + () -> { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + showActionView(viewHolder.mFirstActionContainer); }; if (mLastFocusedViewId == R.id.action_second_container) { mLastFocusedViewId = R.id.info_container; @@ -727,12 +757,9 @@ class ScheduleRowPresenter extends RowPresenter { case 0: default: viewHolder.mPendingAnimationRunnable = - new Runnable() { - @Override - public void run() { - hideActionView(viewHolder.mSecondActionContainer, View.GONE); - hideActionView(viewHolder.mFirstActionContainer, View.GONE); - } + () -> { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + hideActionView(viewHolder.mFirstActionContainer, View.GONE); }; mLastFocusedViewId = R.id.info_container; SoftPreconditions.checkState( diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index eb01aba2..28a44bf3 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -211,13 +211,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean focused) { - view.post( - new Runnable() { - @Override - public void run() { - updateSelector(view); - } - }); + view.post(() -> updateSelector(view)); } }; mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener); diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java index b8b19adc..f24ad2c0 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -74,8 +74,10 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene private Intent createProgramIntent(Intent intent) { if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); - long recordedProgramId = ContentUris.parseId(uri); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId); + if (uri != null) { + long recordedProgramId = ContentUris.parseId(uri); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId); + } } return intent; } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java index 59c90d11..791d26bb 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -39,6 +39,7 @@ import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.util.TimeShiftUtils; import java.util.ArrayList; @@ -53,10 +54,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private static final boolean DEBUG = false; private static final int AUDIO_ACTION_ID = 1001; + private static final long INVALID_TIME = -1; private int mPlaybackState = PlaybackState.STATE_NONE; private int mPlaybackSpeedLevel; private int mPlaybackSpeedId; + private long mProgramStartTimeMs = INVALID_TIME; + private boolean mEnableBuffering = false; private boolean mReadyToControl; private final DvrPlaybackOverlayFragment mFragment; @@ -67,6 +71,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private final MultiAction mClosedCaptioningAction; private final MultiAction mMultiAudioAction; private ArrayObjectAdapter mSecondaryActionsAdapter; + private PlaybackControlsRow mPlaybackControlsRow; + @Nullable private View mPlayPauseButton; DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); @@ -79,13 +85,18 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); mClosedCaptioningAction = new ClosedCaptioningAction(activity); mMultiAudioAction = new MultiAudioAction(activity); + mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs(); + if (mProgramStartTimeMs != INVALID_TIME) { + mEnableBuffering = true; + } createControlsRowPresenter(); } void createControlsRow() { - PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); - setControlsRow(controlsRow); - mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter(); + mPlaybackControlsRow = new PlaybackControlsRow(this); + setControlsRow(mPlaybackControlsRow); + mSecondaryActionsAdapter = + (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter(); } private void createControlsRowPresenter() { @@ -118,6 +129,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); vh.setOnKeyListener(DvrPlaybackControlHelper.this); + ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar); + mPlayPauseButton = controlBar.getChildAt(1); } @Override @@ -265,6 +278,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { getHost().notifyPlaybackRowChanged(); } + /** Update the focus to play pause button. */ + public void onPlaybackResume() { + if (mPlayPauseButton != null) { + mPlayPauseButton.requestFocus(); + } + } + @Nullable Boolean hasSecondaryRow() { if (mSecondaryActionsAdapter == null) { @@ -292,6 +312,15 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls.pause(); } + @Override + public void updateProgress() { + if (mEnableBuffering) { + super.updateProgress(); + long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs; + mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs); + } + } + /** Notifies closed caption being enabled/disabled to update related UI. */ void onSubtitleTrackStateChanged(boolean enabled) { mClosedCaptioningAction.setIndex( diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java index bef036eb..81abb8e4 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -39,9 +39,6 @@ import com.android.tv.util.Utils; import com.android.tv.util.images.ImageLoader; class DvrPlaybackMediaSessionHelper { - private static final String TAG = "DvrPlaybackMediaSessionHelper"; - private static final boolean DEBUG = false; - private int mNowPlayingCardWidth; private int mNowPlayingCardHeight; private int mSpeedLevel; @@ -73,6 +70,9 @@ class DvrPlaybackMediaSessionHelper { @Override public void onPlaybackPositionChanged(long positionMs) { updateMediaSessionPlaybackState(); + if (getProgram().isPartial()) { + overlayFragment.updateProgress(); + } if (mDvrPlayer.isPlaybackPrepared()) { mDvrWatchedPositionManager.setWatchedPosition( mDvrPlayer.getProgram().getId(), positionMs); @@ -94,6 +94,11 @@ class DvrPlaybackMediaSessionHelper { mActivity.startActivity(intent); } } + + @Override + public void onPlaybackResume() { + overlayFragment.onPlaybackResume(); + } }); initializeMediaSession(mediaSessionTag); } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java index d3374cfa..1059e852 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -25,7 +25,6 @@ import android.media.session.PlaybackState; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; -import android.media.tv.TvView; import android.os.Bundle; import android.support.v17.leanback.app.PlaybackFragment; import android.support.v17.leanback.app.PlaybackFragmentGlueHost; @@ -52,7 +51,7 @@ import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.SortedArrayAdapter; import com.android.tv.dvr.ui.browse.DvrListRowPresenter; import com.android.tv.dvr.ui.browse.RecordingCardView; -import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.ui.AppLayerTvView; import com.android.tv.util.TvSettings; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; @@ -66,6 +65,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + private static final long INVALID_TIME = -1; // mProgram is only used to store program from intent. Don't use it elsewhere. private RecordedProgram mProgram; @@ -76,8 +76,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; private DvrDataManager mDvrDataManager; - private ContentRatingsManager mContentRatingsManager; - private TvView mTvView; + private AppLayerTvView mTvView; private View mBlockScreenView; private ListRow mRelatedRecordingsRow; private int mVerticalPaddingBase; @@ -117,10 +116,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { .getDimensionPixelOffset( R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = - TvSingletons.getSingletons(getContext()) - .getTvInputManagerHelper() - .getContentRatingsManager(); if (!mDvrDataManager.isRecordedProgramLoadFinished()) { mDvrDataManager.addRecordedProgramLoadFinishedListener( new DvrDataManager.OnRecordedProgramLoadFinishedListener() { @@ -157,9 +152,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mTvView = getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mDvrPlayer = new DvrPlayer(mTvView); + mDvrPlayer = new DvrPlayer(mTvView, getActivity()); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); @@ -279,6 +274,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { mPlaybackControlHelper.unregisterCallback(); mMediaSessionHelper.release(); mRelatedRecordingCardPresenter.unbindAllViewHolders(); + mDvrPlayer.release(); super.onDestroy(); } @@ -503,6 +499,20 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { } } + public void onPlaybackResume() { + mPlaybackControlHelper.onPlaybackResume(); + } + + public long getProgramStartTimeMs() { + return (mProgram != null && mProgram.isPartial()) + ? mProgram.getStartTimeUtcMillis() + : INVALID_TIME; + } + + public void updateProgress() { + mPlaybackControlHelper.updateProgress(); + } + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java index 85bb31b2..d14646b8 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -16,6 +16,7 @@ package com.android.tv.dvr.ui.playback; +import android.content.Context; import android.media.PlaybackParams; import android.media.session.PlaybackState; import android.media.tv.TvContentRating; @@ -24,12 +25,16 @@ import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.text.TextUtils; import android.util.Log; +import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; +import com.android.tv.dvr.DvrTvView; import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.ui.AppLayerTvView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -class DvrPlayer { +/** Player for recorded programs. */ +public class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; @@ -40,10 +45,11 @@ class DvrPlayer { private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + private static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5); private RecordedProgram mProgram; private long mInitialSeekPositionMs; - private final TvView mTvView; + private final DvrTvView mTvView; private DvrPlayerCallback mCallback; private OnAspectRatioChangedListener mOnAspectRatioChangedListener; private OnContentBlockedListener mOnContentBlockedListener; @@ -63,6 +69,7 @@ class DvrPlayer { private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; private boolean mTimeShiftPlayAvailable; + /** Callback of DVR player. */ public static class DvrPlayerCallback { /** * Called when the playback position is changed. The normal updating frequency is around 1 @@ -74,8 +81,11 @@ class DvrPlayer { public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {} /** Called when the playback toward the end. */ public void onPlaybackEnded() {} + /** Called when the playback is resumed to live position. */ + public void onPlaybackResume() {} } + /** Listener for aspect ratio changed events. */ public interface OnAspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. @@ -86,27 +96,32 @@ class DvrPlayer { void onAspectRatioChanged(float videoAspectRatio); } + /** Listener for content blocked events. */ public interface OnContentBlockedListener { /** Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } + /** Listener for tracks availability changed events */ public interface OnTracksAvailabilityChangedListener { /** Called when the Video's subtitle or audio tracks are changed. */ void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); } + /** Listener for track selected events */ public interface OnTrackSelectedListener { /** Called when certain subtitle or audio track is selected. */ void onTrackSelected(String selectedTrackId); } - public DvrPlayer(TvView tvView) { - mTvView = tvView; + /** Constructor of DvrPlayer. */ + public DvrPlayer(AppLayerTvView tvView, Context context) { + mTvView = new DvrTvView(context, tvView, this); mTvView.setCaptionEnabled(true); mPlaybackParams.setSpeed(1.0f); setTvViewCallbacks(); setCallback(null); + mTvView.init(); } /** @@ -333,7 +348,8 @@ class DvrPlayer { /** Returns the audio tracks of the current playback. */ public ArrayList<TvTrackInfo> getAudioTracks() { - return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO); + return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks); } /** Returns the ID of the selected track of the given type. */ @@ -352,6 +368,10 @@ class DvrPlayer { && mPlaybackState != PlaybackState.STATE_CONNECTING; } + public void release() { + mTvView.release(); + } + /** * Selects the given track. * @@ -426,9 +446,16 @@ class DvrPlayer { resumeToWatchedPositionIfNeeded(); } timeMs -= mStartPositionMs; - if (mPlaybackState == PlaybackState.STATE_REWINDING - && timeMs <= REWIND_POSITION_MARGIN_MS) { + long bufferedTimeMs = + System.currentTimeMillis() + - mProgram.getStartTimeUtcMillis() + - FORWARD_POSITION_MARGIN_MS; + if ((mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) + || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + && timeMs > bufferedTimeMs)) { play(); + mCallback.onPlaybackResume(); } else { mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); @@ -440,7 +467,7 @@ class DvrPlayer { } }); mTvView.setCallback( - new TvView.TvInputCallback() { + new TvInputCallbackCompat() { @Override public void onTimeShiftStatusChanged(String inputId, int status) { if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); diff --git a/src/com/android/tv/features/PartnerFeatures.java b/src/com/android/tv/features/PartnerFeatures.java new file mode 100644 index 00000000..6d680b7b --- /dev/null +++ b/src/com/android/tv/features/PartnerFeatures.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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.features; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import com.android.tv.common.feature.Feature; +import com.google.android.tv.partner.support.PartnerCustomizations; + +/** Features backed by {@link PartnerCustomizations}. */ +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated +public final class PartnerFeatures { + + public static final Feature TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE = + new PartnerFeature( + PartnerCustomizations.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE); + + public static final Feature TURN_OFF_EMBEDDED_TUNER = + new PartnerFeature(PartnerCustomizations.TURN_OFF_EMBEDDED_TUNER); + + public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION = + new PartnerFeature(PartnerCustomizations.TVPROVIDER_ALLOWS_COLUMN_CREATION); + + private static class PartnerFeature implements Feature { + + private final String property; + + public PartnerFeature(String property) { + this.property = property; + } + + @Override + public boolean isEnabled(Context context) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + PartnerCustomizations partnerCustomizations = new PartnerCustomizations(context); + return partnerCustomizations.getBooleanResource(context, property).orElse(false); + } + return false; + } + } + + private PartnerFeatures() {} +} diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java index d2cf76e7..208d53f6 100644 --- a/src/com/android/tv/TvFeatures.java +++ b/src/com/android/tv/features/TvFeatures.java @@ -14,13 +14,15 @@ * limitations under the License */ -package com.android.tv; +package com.android.tv.features; -import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE; -import static com.android.tv.common.feature.FeatureUtils.AND; +import static com.android.tv.common.feature.BuildTypeFeature.ASOP_FEATURE; +import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE; import static com.android.tv.common.feature.FeatureUtils.OFF; import static com.android.tv.common.feature.FeatureUtils.ON; -import static com.android.tv.common.feature.FeatureUtils.OR; +import static com.android.tv.common.feature.FeatureUtils.and; +import static com.android.tv.common.feature.FeatureUtils.not; +import static com.android.tv.common.feature.FeatureUtils.or; import android.content.Context; import android.content.pm.PackageManager; @@ -31,14 +33,14 @@ import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.feature.ExperimentFeature; import com.android.tv.common.feature.Feature; import com.android.tv.common.feature.FeatureUtils; -import com.android.tv.common.feature.GServiceFeature; +import com.android.tv.common.feature.FlagFeature; import com.android.tv.common.feature.PropertyFeature; import com.android.tv.common.feature.Sdk; import com.android.tv.common.feature.TestableFeature; +import com.android.tv.common.flags.has.HasUiFlags; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.common.util.PermissionUtils; -import com.google.android.tv.partner.support.PartnerCustomizations; - /** * List of {@link Feature} for the Live TV App. * @@ -46,33 +48,48 @@ import com.google.android.tv.partner.support.PartnerCustomizations; */ public final class TvFeatures extends CommonFeatures { + /** When enabled store network affiliation information to TV provider */ + public static final Feature STORE_NETWORK_AFFILIATION = ENG_ONLY_FEATURE; + /** When enabled use system setting for turning on analytics. */ public static final Feature ANALYTICS_OPT_IN = ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX); - /** When enabled shows a list of failed recordings */ - public static final Feature DVR_FAILED_LIST = ENG_ONLY_FEATURE; /** * Analytics that include sensitive information such as channel or program identifiers. * * <p>See <a href="http://b/22062676">b/22062676</a> */ - public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN); + public static final Feature ANALYTICS_V2 = and(ON, ANALYTICS_OPT_IN); + + private static final Feature TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE = + or(Sdk.AT_LEAST_O, PartnerFeatures.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE); + + /** + * Enable cloud EPG for third parties. + * + * @see <a href="http://go/cloud-epg-3p-proposal">go/cloud-epg-3p-proposal</a> + */ + // TODO verify customization for N + public static final TestableFeature CLOUD_EPG_FOR_3RD_PARTY = + TestableFeature.createTestableFeature( + and( + not(ASOP_FEATURE), + // TODO(b/66696290): use newer version of robolectric. + or( + TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE, + FeatureUtils.ROBOLECTRIC))); - public static final Feature EPG_SEARCH = - PropertyFeature.create("feature_tv_use_epg_search", false); + // TODO(b/76149661): Fix EPG search or remove it + public static final Feature EPG_SEARCH = OFF; - private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide"; /** A flag which indicates that LC app is unhidden even when there is no input. */ public static final Feature UNHIDE = - OR( - new GServiceFeature(GSERVICE_KEY_UNHIDE, false), - new Feature() { - @Override - public boolean isEnabled(Context context) { - // If LC app runs as non-system app, we unhide the app. - return !PermissionUtils.hasAccessAllEpg(context); - } - }); + or( + FlagFeature.from( + context -> HasSingletons.get(HasUiFlags.class, context), + input -> input.getUiFlags().uhideLauncher()), + // If LC app runs as non-system app, we unhide the app. + not(PermissionUtils::hasAccessAllEpg)); public static final Feature PICTURE_IN_PICTURE = new Feature() { diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 5b53f904..bc1b11b6 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -47,7 +47,7 @@ import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeL import com.android.tv.ChannelTuner; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.common.util.DurationTimer; @@ -56,11 +56,16 @@ import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.features.TvFeatures; +import com.android.tv.perf.EventNames; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.TimerEvent; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; import com.android.tv.ui.ViewUtils; import com.android.tv.ui.hideable.AutoHideScheduler; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import com.android.tv.common.flags.BackendKnobsFlags; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -150,6 +155,9 @@ public class ProgramGuide private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener(); + private final PerformanceMonitor mPerformanceMonitor; + private TimerEvent mTimerEvent; + private final Runnable mUpdateTimeIndicator = new Runnable() { @Override @@ -175,13 +183,17 @@ public class ProgramGuide Runnable preShowRunnable, Runnable postHideRunnable) { mActivity = activity; + TvSingletons singletons = TvSingletons.getSingletons(mActivity); + mPerformanceMonitor = singletons.getPerformanceMonitor(); + BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs(); mProgramManager = new ProgramManager( tvInputManagerHelper, channelDataManager, programDataManager, dvrDataManager, - dvrScheduleManager); + dvrScheduleManager, + backendKnobsFlags); mChannelTuner = channelTuner; mTracker = tracker; mPreShowRunnable = preShowRunnable; @@ -316,12 +328,43 @@ public class ProgramGuide mGrid.setItemAlignmentOffset(0); mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); + mGrid.addOnScrollListener( + new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (DEBUG) { + Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState); + } + if (newState == RecyclerView.SCROLL_STATE_SETTLING) { + mPerformanceMonitor.startJankRecorder( + EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY); + } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { + mPerformanceMonitor.stopJankRecorder( + EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY); + } + } + }); + RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { onHorizontalScrolled(dx); } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (DEBUG) { + Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState); + } + if (newState == RecyclerView.SCROLL_STATE_SETTLING) { + mPerformanceMonitor.startJankRecorder( + EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY); + } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { + mPerformanceMonitor.stopJankRecorder( + EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY); + } + } }; mTimelineRow.addOnScrollListener(onScrollListener); @@ -332,6 +375,18 @@ public class ProgramGuide R.animator.program_guide_side_panel_enter_full, 0, R.animator.program_guide_table_enter_full); + mShowAnimatorFull.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mTimerEvent != null) { + mPerformanceMonitor.stopTimer( + mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW); + mTimerEvent = null; + } + mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); + } + }); mShowAnimatorPartial = createAnimator( @@ -345,6 +400,16 @@ public class ProgramGuide mSidePanelGridView.setVisibility(View.VISIBLE); mSidePanelGridView.setAlpha(1.0f); } + + @Override + public void onAnimationEnd(Animator animation) { + if (mTimerEvent != null) { + mPerformanceMonitor.stopTimer( + mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW); + mTimerEvent = null; + } + mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); + } }); mHideAnimatorFull = @@ -355,6 +420,11 @@ public class ProgramGuide mHideAnimatorFull.addListener( new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE); + } + + @Override public void onAnimationEnd(Animator animation) { mContainer.setVisibility(View.GONE); } @@ -367,6 +437,11 @@ public class ProgramGuide mHideAnimatorPartial.addListener( new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE); + } + + @Override public void onAnimationEnd(Animator animation) { mContainer.setVisibility(View.GONE); } @@ -447,6 +522,8 @@ public class ProgramGuide if (mContainer.getVisibility() == View.VISIBLE) { return; } + mTimerEvent = mPerformanceMonitor.startTimer(); + mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); mTracker.sendShowEpg(); mTracker.sendScreenView(SCREEN_NAME); if (mPreShowRunnable != null) { @@ -643,6 +720,11 @@ public class ProgramGuide return mGrid; } + /** Returns if Accessibility is enabled. */ + boolean isAccessibilityEnabled() { + return mAccessibilityManager.isEnabled(); + } + /** Gets {@link VerticalGridView} for "genre select" side panel. */ VerticalGridView getSidePanel() { return mSidePanelGridView; @@ -711,9 +793,7 @@ public class ProgramGuide } private void startFull() { - if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) { - // If accessibility service is enabled, focus cannot be moved to side panel due to it's - // hidden. Therefore, we don't hide side panel when accessibility service is enabled. + if (!mShowGuidePartial) { return; } mShowGuidePartial = false; @@ -806,13 +886,7 @@ public class ProgramGuide detailView.setVisibility(View.VISIBLE); final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row); - programRow.post( - new Runnable() { - @Override - public void run() { - programRow.focusCurrentProgram(); - } - }); + programRow.post(programRow::focusCurrentProgram); } else { animateRowChange(mSelectedRow, row); } @@ -935,6 +1009,7 @@ public class ProgramGuide private static final int UNKNOWN = 0; private static final int SIDE_PANEL = 1; private static final int PROGRAM_TABLE = 2; + private static final int CHANNEL_COLUMN = 3; @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { @@ -948,6 +1023,10 @@ public class ProgramGuide startFull(); } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) { startPartial(); + } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) { + startFull(); + } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) { + startPartial(); } } @@ -959,7 +1038,11 @@ public class ProgramGuide if (obj == mSidePanel) { return SIDE_PANEL; } else if (obj == mGrid) { - return PROGRAM_TABLE; + if (view instanceof ProgramItemView) { + return PROGRAM_TABLE; + } else { + return CHANNEL_COLUMN; + } } } return UNKNOWN; diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 9f379e43..a46beab7 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -103,12 +103,9 @@ public class ProgramItemView extends TextView { tvActivity.getChannelDataManager().getChannel(entry.channelId); if (entry.isCurrentProgram()) { view.postDelayed( - new Runnable() { - @Override - public void run() { - tvActivity.tuneToChannel(channel); - tvActivity.hideOverlaysForTune(); - } + () -> { + tvActivity.tuneToChannel(channel); + tvActivity.hideOverlaysForTune(); }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 @@ -125,13 +122,9 @@ public class ProgramItemView extends TextView { DvrUiHelper.checkStorageStatusAndShowErrorMessage( tvActivity, channel.getInputId(), - new Runnable() { - @Override - public void run() { + () -> DvrUiHelper.requestRecordingFutureProgram( - tvActivity, entry.program, false); - } - }); + tvActivity, entry.program, false)); } else { dvrManager.removeScheduledRecording(entry.scheduledRecording); String msg = @@ -378,7 +371,7 @@ public class ProgramItemView extends TextView { int iconResId = 0; if (isEntryWideEnough() && mTableEntry.scheduledRecording != null) { if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { - iconResId = R.drawable.ic_warning_white_18dp; + iconResId = R.drawable.quantum_ic_warning_white_18; } else { switch (mTableEntry.scheduledRecording.getState()) { case ScheduledRecording.STATE_RECORDING_NOT_STARTED: @@ -405,20 +398,22 @@ public class ProgramItemView extends TextView { if (channel != null) { description = channel.getDisplayNumber() + " " + description; } - description += - " " - + Utils.getDurationString( - getContext(), - mClock, - mTableEntry.entryStartUtcMillis, - mTableEntry.entryEndUtcMillis, - true); Program program = mTableEntry.program; if (program != null) { + description += " " + program.getDurationString(getContext()); String episodeDescription = program.getEpisodeContentDescription(getContext()); if (!TextUtils.isEmpty(episodeDescription)) { description += " " + episodeDescription; } + } else { + description += + " " + + Utils.getDurationString( + getContext(), + mClock, + mTableEntry.entryStartUtcMillis, + mTableEntry.entryEndUtcMillis, + true); } if (mTableEntry.scheduledRecording != null) { if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index 3f20a837..3a5a4a02 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -32,6 +32,7 @@ import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import com.android.tv.common.flags.BackendKnobsFlags; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,6 +60,7 @@ public class ProgramManager { private final ProgramDataManager mProgramDataManager; private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled private final DvrScheduleManager mDvrScheduleManager; + private final BackendKnobsFlags mBackendKnobsFlags; private long mStartUtcMillis; private long mEndUtcMillis; @@ -114,12 +116,26 @@ public class ProgramManager { } }; - private final ProgramDataManager.Listener mProgramDataManagerListener = - new ProgramDataManager.Listener() { + private final ProgramDataManager.Callback mProgramDataManagerCallback = + new ProgramDataManager.Callback() { @Override public void onProgramUpdated() { updateTableEntries(true); } + + @Override + public void onSingleChannelUpdated(long channelId) { + boolean parentalControlsEnabled = + mTvInputManagerHelper + .getParentalControlSettings() + .isParentalControlsEnabled(); + // Inline the updating of the mChannelIdEntriesMap here so we can only call + // getParentalControlSettings once. + List<TableEntry> entries = + createProgramEntries(channelId, parentalControlsEnabled); + mChannelIdEntriesMap.put(channelId, entries); + notifyTableEntriesUpdated(); + } }; private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = @@ -199,19 +215,21 @@ public class ProgramManager { ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, - @Nullable DvrScheduleManager dvrScheduleManager) { + @Nullable DvrScheduleManager dvrScheduleManager, + BackendKnobsFlags backendKnobsFlags) { mTvInputManagerHelper = tvInputManagerHelper; mChannelDataManager = channelDataManager; mProgramDataManager = programDataManager; mDvrDataManager = dvrDataManager; mDvrScheduleManager = dvrScheduleManager; + mBackendKnobsFlags = backendKnobsFlags; } void programGuideVisibilityChanged(boolean visible) { mProgramDataManager.setPauseProgramUpdate(visible); if (visible) { mChannelDataManager.addListener(mChannelDataManagerListener); - mProgramDataManager.addListener(mProgramDataManagerListener); + mProgramDataManager.addCallback(mProgramDataManagerCallback); if (mDvrDataManager != null) { if (!mDvrDataManager.isDvrScheduleLoadFinished()) { mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener); @@ -224,7 +242,7 @@ public class ProgramManager { } } else { mChannelDataManager.removeListener(mChannelDataManagerListener); - mProgramDataManager.removeListener(mProgramDataManagerListener); + mProgramDataManager.removeCallback(mProgramDataManagerCallback); if (mDvrDataManager != null) { mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener); mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); @@ -233,6 +251,7 @@ public class ProgramManager { mDvrScheduleManager.removeOnConflictStateChangeListener( mOnConflictStateChangeListener); } + mChannelIdEntriesMap.clear(); } } @@ -309,8 +328,8 @@ public class ProgramManager { long fromUtcMillis = mFromUtcMillis + timeMillisToScroll; long toUtcMillis = mToUtcMillis + timeMillisToScroll; if (fromUtcMillis < mStartUtcMillis) { - fromUtcMillis = mStartUtcMillis; toUtcMillis += mStartUtcMillis - fromUtcMillis; + fromUtcMillis = mStartUtcMillis; } if (toUtcMillis > mEndUtcMillis) { fromUtcMillis -= toUtcMillis - mEndUtcMillis; @@ -345,10 +364,12 @@ public class ProgramManager { /** Returns the program index of the program at {@code time} or -1 if not found. */ int getProgramIndexAtTime(long channelId, long time) { List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); - for (int i = 0; i < entries.size(); ++i) { - TableEntry entry = entries.get(i); - if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) { - return i; + if (entries != null) { + for (int i = 0; i < entries.size(); ++i) { + TableEntry entry = entries.get(i); + if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) { + return i; + } } } return -1; @@ -401,7 +422,7 @@ public class ProgramManager { * given {@code channelId}. */ int getTableEntryCount(long channelId) { - return mChannelIdEntriesMap.get(channelId).size(); + return mChannelIdEntriesMap.isEmpty() ? 0 : mChannelIdEntriesMap.get(channelId).size(); } /** @@ -410,6 +431,9 @@ public class ProgramManager { * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs. */ TableEntry getTableEntry(long channelId, int index) { + if (mBackendKnobsFlags.enablePartialProgramFetch()) { + mProgramDataManager.prefetchChannel(channelId); + } return mChannelIdEntriesMap.get(channelId).get(index); } @@ -437,6 +461,14 @@ public class ProgramManager { buildGenreFilters(); } + /** Sets the channel list for testing */ + void setChannels(List<Channel> channels) { + mChannels = new ArrayList<>(channels); + mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; + mFilteredChannels = mChannels; + buildGenreFilters(); + } + private void updateTableEntries(boolean clear) { updateTableEntriesWithoutNotification(clear); notifyTableEntriesUpdated(); @@ -544,6 +576,9 @@ public class ProgramManager { @Nullable private TableEntry getTableEntry(long channelId, long entryId) { + if (mChannelIdEntriesMap.isEmpty()) { + return null; + } List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); if (entries != null) { for (TableEntry entry : entries) { diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index 83175bb6..3317c15f 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -72,6 +72,9 @@ public class ProgramRow extends TimelineGridView { public ProgramRow(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + ProgramRowAccessibilityDelegate rowAccessibilityDelegate = + new ProgramRowAccessibilityDelegate(this); + this.setAccessibilityDelegateCompat(rowAccessibilityDelegate); } /** Registers a listener focus events occurring on children to the {@code ProgramRow}. */ @@ -126,13 +129,26 @@ public class ProgramRow extends TimelineGridView { : direction == View.FOCUS_LEFT; } + // When Accessibility is enabled, this API will keep next node visible + void focusSearchAccessibility(View focused, int direction) { + TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry(); + long toMillis = mProgramManager.getToUtcMillis(); + + if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) { + if (focusedEntry.entryEndUtcMillis >= toMillis) { + scrollByTime(focusedEntry.entryEndUtcMillis - toMillis + HALF_HOUR_MILLIS); + } + } + } + @Override public View focusSearch(View focused, int direction) { TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry(); long fromMillis = mProgramManager.getFromUtcMillis(); long toMillis = mProgramManager.getToUtcMillis(); - if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) { + if (!mProgramGuide.isAccessibilityEnabled() + && (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD)) { if (focusedEntry.entryStartUtcMillis < fromMillis) { // The current entry starts outside of the view; Align or scroll to the left. scrollByTime( @@ -162,7 +178,9 @@ public class ProgramRow extends TimelineGridView { TableEntry targetEntry = ((ProgramItemView) target).getTableEntry(); if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) { - if (targetEntry.entryStartUtcMillis < fromMillis + if (mProgramGuide.isAccessibilityEnabled()) { + scrollByTime(targetEntry.entryStartUtcMillis - fromMillis); + } else if (targetEntry.entryStartUtcMillis < fromMillis && targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) { // The target entry starts outside the view; Align or scroll to the left. scrollByTime( diff --git a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java new file mode 100644 index 00000000..5e498be4 --- /dev/null +++ b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2015 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.guide; + +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerViewAccessibilityDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** AccessibilityDelegate for {@link ProgramRow} */ +class ProgramRowAccessibilityDelegate extends RecyclerViewAccessibilityDelegate { + private final ItemDelegate mItemDelegate; + + ProgramRowAccessibilityDelegate(RecyclerView recyclerView) { + super(recyclerView); + + mItemDelegate = + new ItemDelegate(this) { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + // Prevent Accessibility service to move the Program Row elements + // Ignoring Accessibility action above Set Text + // (accessibilityActionShowOnScreen) + if (action > AccessibilityNodeInfo.ACTION_SET_TEXT) { + return false; + } + + return super.performAccessibilityAction(host, action, args); + } + }; + } + + @Override + public ItemDelegate getItemDelegate() { + return mItemDelegate; + } + + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup host, View child, AccessibilityEvent event) { + // Forcing the next item to be visible for scrolling in forward direction + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + ((ProgramRow) host).focusSearchAccessibility(child, View.FOCUS_FORWARD); + } + return super.onRequestSendAccessibilityEvent(host, child, event); + } +} diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index 6e7485ac..7576bf50 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -110,6 +110,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr private final int mDvrPaddingStartWithTrack; private final int mDvrPaddingStartWithOutTrack; + private RecyclerView mRecyclerView; + ProgramTableAdapter(Context context, ProgramGuide programGuide) { mContext = context; mAccessibilityManager = @@ -198,7 +200,15 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mProgramManager.addTableEntriesUpdatedListener(listAdapter); mProgramListAdapters.add(listAdapter); } - notifyDataSetChanged(); + if (mRecyclerView != null && mRecyclerView.isComputingLayout()) { + // it means that RecyclerView is in a lockdown state and any attempt to update adapter + // contents will result in an exception because adapter contents cannot be changed while + // RecyclerView is trying to compute the layout + // postpone the change using a Handler + mHandler.post(this::notifyDataSetChanged); + } else { + notifyDataSetChanged(); + } } @Override @@ -238,8 +248,22 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId); int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId()); if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")"); - mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); - notifyItemChanged(channelIndex, true); + if (channelIndex >= 0 && channelIndex < mProgramListAdapters.size()) { + mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); + notifyItemChanged(channelIndex, true); + } + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + super.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + mRecyclerView = null; } class ProgramRowViewHolder extends RecyclerView.ViewHolder @@ -260,13 +284,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr } } }; - private final Runnable mUpdateDetailViewRunnable = - new Runnable() { - @Override - public void run() { - updateDetailView(); - } - }; + private final Runnable mUpdateDetailViewRunnable = this::updateDetailView; private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @@ -420,12 +438,14 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mChannelNumberView.setText(displayNumber); mChannelNumberView.setVisibility(View.VISIBLE); } + + boolean isChannelLocked = isChannelLocked(channel); mChannelNumberView.setTextColor( - isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor); + isChannelLocked ? mChannelBlockedTextColor : mChannelTextColor); mChannelLogoView.setImageBitmap(null); mChannelLogoView.setVisibility(View.GONE); - if (isChannelLocked(channel)) { + if (isChannelLocked) { mChannelNameView.setVisibility(View.GONE); mChannelBlockView.setVisibility(View.VISIBLE); } else { @@ -573,13 +593,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mTitleView.setText(text); } - updateTextView( - mTimeView, - Utils.getDurationString( - context, - program.getStartTimeUtcMillis(), - program.getEndTimeUtcMillis(), - false)); + updateTextView(mTimeView, program.getDurationString(context)); boolean trackMetaDataVisible = updateTextView( diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 8536ef1f..4a9e4765 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -20,6 +20,9 @@ import android.content.Context; import android.content.Intent; import android.media.tv.TvInputInfo; import android.view.View; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import com.android.tv.ChannelChanger; import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; @@ -34,9 +37,8 @@ import java.util.ArrayList; import java.util.List; /** An adapter of the Channels row. */ -public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> { - // There are four special cards: guide, setup, dvr, applink. - private static final int SIZE_OF_VIEW_TYPE = 5; +public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> + implements AccessibilityStateChangeListener { private final Context mContext; private final Tracker mTracker; @@ -44,58 +46,9 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels private final DvrDataManager mDvrDataManager; private final int mMaxCount; private final int mMinCount; + private final ChannelChanger mChannelChanger; - private final View.OnClickListener mGuideOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_program_guide); - getMainActivity().getOverlayManager().showProgramGuide(); - } - }; - - private final View.OnClickListener mSetupOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_setup); - getMainActivity().getOverlayManager().showSetupFragment(); - } - }; - - private final View.OnClickListener mDvrOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_dvr); - getMainActivity().getOverlayManager().showDvrManager(); - } - }; - - private final View.OnClickListener mAppLinkOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_app_link); - Intent intent = ((AppLinkCardView) view).getIntent(); - if (intent != null) { - getMainActivity().startActivitySafe(intent); - } - } - }; - - private final View.OnClickListener mChannelOnClickListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - // Always send the label "Channels" because the channel ID or name or number - // might be - // sensitive. - mTracker.sendMenuClicked(R.string.menu_title_channels); - getMainActivity().tuneToChannel((Channel) view.getTag()); - getMainActivity().hideOverlaysForTune(); - } - }; + private boolean mShowChannelUpDown; public ChannelsRowAdapter( Context context, Recommender recommender, int minCount, int maxCount) { @@ -112,6 +65,11 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels mMinCount = minCount; mMaxCount = maxCount; setHasStableIds(true); + mChannelChanger = (ChannelChanger) (context); + AccessibilityManager accessibilityManager = + context.getSystemService(AccessibilityManager.class); + mShowChannelUpDown = accessibilityManager.isEnabled(); + accessibilityManager.addAccessibilityStateChangeListener(this); } @Override @@ -133,18 +91,22 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels public void onBindViewHolder(MyViewHolder viewHolder, int position) { int viewType = getItemViewType(position); if (viewType == R.layout.menu_card_guide) { - viewHolder.itemView.setOnClickListener(mGuideOnClickListener); + viewHolder.itemView.setOnClickListener(this::onGuideClicked); + } else if (viewType == R.layout.menu_card_up) { + viewHolder.itemView.setOnClickListener(this::onChannelUpClicked); + } else if (viewType == R.layout.menu_card_down) { + viewHolder.itemView.setOnClickListener(this::onChannelDownClicked); } else if (viewType == R.layout.menu_card_setup) { - viewHolder.itemView.setOnClickListener(mSetupOnClickListener); + viewHolder.itemView.setOnClickListener(this::onSetupClicked); } else if (viewType == R.layout.menu_card_app_link) { - viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener); + viewHolder.itemView.setOnClickListener(this::onAppLinkClicked); } else if (viewType == R.layout.menu_card_dvr) { - viewHolder.itemView.setOnClickListener(mDvrOnClickListener); + viewHolder.itemView.setOnClickListener(this::onDvrClicked); SimpleCardView view = (SimpleCardView) viewHolder.itemView; view.setText(R.string.channels_item_dvr); } else { viewHolder.itemView.setTag(getItemList().get(position).getChannel()); - viewHolder.itemView.setOnClickListener(mChannelOnClickListener); + viewHolder.itemView.setOnClickListener(this::onChannelClicked); } super.onBindViewHolder(viewHolder, position); } @@ -158,9 +120,53 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels } } + private void onGuideClicked(View unused) { + mTracker.sendMenuClicked(R.string.channels_item_program_guide); + getMainActivity().getOverlayManager().showProgramGuide(); + } + + private void onChannelDownClicked(View unused) { + mChannelChanger.channelDown(); + } + + private void onChannelUpClicked(View unused) { + mChannelChanger.channelUp(); + } + + private void onSetupClicked(View unused) { + mTracker.sendMenuClicked(R.string.channels_item_setup); + getMainActivity().getOverlayManager().showSetupFragment(); + } + + private void onDvrClicked(View unused) { + mTracker.sendMenuClicked(R.string.channels_item_dvr); + getMainActivity().getOverlayManager().showDvrManager(); + } + + private void onAppLinkClicked(View view) { + mTracker.sendMenuClicked(R.string.channels_item_app_link); + Intent intent = ((AppLinkCardView) view).getIntent(); + if (intent != null) { + getMainActivity().startActivitySafe(intent); + } + } + + private void onChannelClicked(View view) { + // Always send the label "Channels" because the channel ID or name or number might be + // sensitive. + mTracker.sendMenuClicked(R.string.menu_title_channels); + getMainActivity().tuneToChannel((Channel) view.getTag()); + getMainActivity().hideOverlaysForTune(); + } + private void createItems() { List<ChannelsRowItem> items = new ArrayList<>(); items.add(ChannelsRowItem.GUIDE_ITEM); + if (mShowChannelUpDown) { + items.add(ChannelsRowItem.UP_ITEM); + items.add(ChannelsRowItem.DOWN_ITEM); + } + if (needToShowSetupItem()) { items.add(ChannelsRowItem.SETUP_ITEM); } @@ -183,6 +189,12 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels // The current index of the item list to iterate. It starts from 1 because the first item // (GUIDE) is always visible and not updated. int currentIndex = 1; + if (updateItem(mShowChannelUpDown, ChannelsRowItem.UP_ITEM, currentIndex)) { + ++currentIndex; + } + if (updateItem(mShowChannelUpDown, ChannelsRowItem.DOWN_ITEM, currentIndex)) { + ++currentIndex; + } if (updateItem(needToShowSetupItem(), ChannelsRowItem.SETUP_ITEM, currentIndex)) { ++currentIndex; } @@ -298,4 +310,10 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels channelList.add(channel); return true; } + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mShowChannelUpDown = enabled; + update(); + } } diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java index 608bb36e..12976ef2 100644 --- a/src/com/android/tv/menu/ChannelsRowItem.java +++ b/src/com/android/tv/menu/ChannelsRowItem.java @@ -30,6 +30,10 @@ public class ChannelsRowItem { public static final int DVR_ITEM_ID = -3; /** The item ID for app link item */ public static final int APP_LINK_ITEM_ID = -4; + /** The item ID for channel up item */ + public static final int UP_ID = -5; + /** The item ID for app link item */ + public static final int DOWN_ID = -6; /** The item which represents the guide. */ public static final ChannelsRowItem GUIDE_ITEM = @@ -44,6 +48,12 @@ public class ChannelsRowItem { public static final ChannelsRowItem APP_LINK_ITEM = new ChannelsRowItem(APP_LINK_ITEM_ID, R.layout.menu_card_app_link); + /** The item which represents the channel up. */ + public static final ChannelsRowItem UP_ITEM = new ChannelsRowItem(UP_ID, R.layout.menu_card_up); + /** The item which represents the channel down. */ + public static final ChannelsRowItem DOWN_ITEM = + new ChannelsRowItem(DOWN_ID, R.layout.menu_card_down); + private final long mItemId; @NonNull private Channel mChannel; private final int mLayoutId; diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 19a93dbc..6bdbf87b 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -213,12 +213,9 @@ public class Menu implements AccessibilityStateChangeListener { rowIdToSelect, mAnimationDisabledForTest ? null - : new Runnable() { - @Override - public void run() { - if (isActive()) { - mShowAnimator.start(); - } + : () -> { + if (isActive()) { + mShowAnimator.start(); } }); scheduleHide(); diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java index 52372535..8c180cae 100644 --- a/src/com/android/tv/menu/MenuAction.java +++ b/src/com/android/tv/menu/MenuAction.java @@ -50,12 +50,12 @@ public class MenuAction { new MenuAction( R.string.options_item_more_channels, TvOptionsManager.OPTION_MORE_CHANNELS, - R.drawable.ic_store); + R.drawable.ic_app_store); public static final MenuAction DEV_ACTION = new MenuAction( R.string.options_item_developer, TvOptionsManager.OPTION_DEVELOPER, - R.drawable.ic_developer_mode_tv_white_48dp); + R.drawable.quantum_ic_developer_mode_tv_white_48); public static final MenuAction SETTINGS_ACTION = new MenuAction( R.string.options_item_settings, diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java index ceffe861..4e69a601 100644 --- a/src/com/android/tv/menu/OptionsRowAdapter.java +++ b/src/com/android/tv/menu/OptionsRowAdapter.java @@ -37,17 +37,14 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< public void onClick(View view) { final MenuAction action = (MenuAction) view.getTag(); view.post( - new Runnable() { - @Override - public void run() { - int resId = action.getActionNameResId(); - if (resId == 0) { - mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL); - } else { - mTracker.sendMenuClicked(resId); - } - executeAction(action.getType()); + () -> { + int resId = action.getActionNameResId(); + if (resId == 0) { + mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL); + } else { + mTracker.sendMenuClicked(resId); } + executeAction(action.getType()); }); } }; diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index 496d1969..0ce74ae1 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -185,13 +185,10 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.lb_ic_skip_previous, R.string.play_controls_description_skip_previous, null, - new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.jumpToPrevious(); - updateControls(true); - } + () -> { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.jumpToPrevious(); + updateControls(true); } }); initializeButton( @@ -199,13 +196,10 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.lb_ic_fast_rewind, R.string.play_controls_description_fast_rewind, null, - new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.rewind(); - updateButtons(); - } + () -> { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.rewind(); + updateButtons(); } }); initializeButton( @@ -213,13 +207,10 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.lb_ic_play, R.string.play_controls_description_play_pause, null, - new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.togglePlayPause(); - updateButtons(); - } + () -> { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.togglePlayPause(); + updateButtons(); } }); initializeButton( @@ -227,13 +218,10 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.lb_ic_fast_forward, R.string.play_controls_description_fast_forward, null, - new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.fastForward(); - updateButtons(); - } + () -> { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.fastForward(); + updateButtons(); } }); initializeButton( @@ -241,13 +229,10 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.lb_ic_skip_next, R.string.play_controls_description_skip_next, null, - new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.jumpToNext(); - updateControls(true); - } + () -> { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.jumpToNext(); + updateControls(true); } }); int color = @@ -257,12 +242,7 @@ public class PlayControlsRowView extends MenuRowView { R.drawable.ic_record_start, R.string.channels_item_record_start, color, - new Runnable() { - @Override - public void run() { - onRecordButtonClicked(); - } - }); + this::onRecordButtonClicked); } private boolean isCurrentChannelRecording() { @@ -296,13 +276,9 @@ public class PlayControlsRowView extends MenuRowView { DvrUiHelper.checkStorageStatusAndShowErrorMessage( mMainActivity, currentChannel.getInputId(), - new Runnable() { - @Override - public void run() { + () -> DvrUiHelper.requestRecordingCurrentProgram( - mMainActivity, currentChannel, program, true); - } - }); + mMainActivity, currentChannel, program, true)); } } else if (currentChannel != null) { DvrUiHelper.showStopRecordingDialog( @@ -490,15 +466,12 @@ public class PlayControlsRowView extends MenuRowView { // After the focus is actually changed, hideRippleAnimation should run // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted. post( - new Runnable() { - @Override - public void run() { - mJumpPreviousButton.hideRippleAnimation(); - mRewindButton.hideRippleAnimation(); - mPlayPauseButton.hideRippleAnimation(); - mFastForwardButton.hideRippleAnimation(); - mJumpNextButton.hideRippleAnimation(); - } + () -> { + mJumpPreviousButton.hideRippleAnimation(); + mRewindButton.hideRippleAnimation(); + mPlayPauseButton.hideRippleAnimation(); + mFastForwardButton.hideRippleAnimation(); + mJumpNextButton.hideRippleAnimation(); }); } diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index 55affb59..fe52b25e 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -18,12 +18,11 @@ package com.android.tv.menu; import android.content.Context; import android.media.tv.TvTrackInfo; -import android.support.annotation.VisibleForTesting; -import com.android.tv.TvFeatures; import com.android.tv.TvOptionsManager; import com.android.tv.common.customization.CustomAction; import com.android.tv.common.util.CommonUtils; import com.android.tv.data.DisplayMode; +import com.android.tv.features.TvFeatures; import com.android.tv.ui.TvViewUiManager; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; import com.android.tv.ui.sidepanel.DeveloperOptionFragment; @@ -78,7 +77,6 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { } } - @VisibleForTesting private boolean updateClosedCaptionAction() { return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION); } diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java new file mode 100644 index 00000000..45383ae1 --- /dev/null +++ b/src/com/android/tv/modules/TvApplicationModule.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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.modules; + +import android.content.Context; +import com.android.tv.MainActivity; +import com.android.tv.TvApplication; +import com.android.tv.common.concurrent.NamedThreadFactory; +import com.android.tv.common.dagger.ApplicationModule; +import com.android.tv.common.dagger.annotations.ApplicationContext; +import com.android.tv.onboarding.OnboardingActivity; +import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.TvInputManagerHelper; +import dagger.Module; +import dagger.Provides; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import javax.inject.Singleton; + +/** Dagger module for {@link TvApplication}. */ +@Module( + includes = { + ApplicationModule.class, + TvSingletonsModule.class, + MainActivity.Module.class, + OnboardingActivity.Module.class + }) +public class TvApplicationModule { + private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db"); + + @Provides + @AsyncDbTask.DbExecutor + @Singleton + Executor providesDbExecutor() { + return Executors.newSingleThreadExecutor(THREAD_FACTORY); + } + + @Provides + @Singleton + TvInputManagerHelper providesTvInputManagerHelper(@ApplicationContext Context context) { + TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context); + tvInputManagerHelper.start(); + return tvInputManagerHelper; + } +} diff --git a/src/com/android/tv/modules/TvSingletonsModule.java b/src/com/android/tv/modules/TvSingletonsModule.java new file mode 100644 index 00000000..f998c08b --- /dev/null +++ b/src/com/android/tv/modules/TvSingletonsModule.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.modules; + +import com.android.tv.TvSingletons; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ProgramDataManager; +import dagger.Module; +import dagger.Provides; + +/** + * Provides bindings for items provided by {@link TvSingletons}. + * + * <p>Use this module to inject items directly instead of using {@code TvSingletons}. + */ +@Module +@SuppressWarnings("deprecation") +public class TvSingletonsModule { + private final TvSingletons mTvSingletons; + + public TvSingletonsModule(TvSingletons mTvSingletons) { + this.mTvSingletons = mTvSingletons; + } + + @Provides + ChannelDataManager providesChannelDataManager() { + return mTvSingletons.getChannelDataManager(); + } + + @Provides + ProgramDataManager providesProgramDataManager() { + return mTvSingletons.getProgramDataManager(); + } +} diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index a1cf9de1..776ae664 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -37,6 +37,9 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; +import dagger.android.AndroidInjection; +import dagger.android.ContributesAndroidInjector; +import javax.inject.Inject; public class OnboardingActivity extends SetupActivity { private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion"; @@ -47,9 +50,9 @@ public class OnboardingActivity extends SetupActivity { private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; - private ChannelDataManager mChannelDataManager; + @Inject ChannelDataManager mChannelDataManager; private TvInputManagerHelper mInputManager; - private SetupUtils mSetupUtils; + @Inject SetupUtils mSetupUtils; private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { @Override @@ -80,12 +83,11 @@ public class OnboardingActivity extends SetupActivity { @Override protected void onCreate(Bundle savedInstanceState) { + AndroidInjection.inject(this); super.onCreate(savedInstanceState); TvSingletons singletons = TvSingletons.getSingletons(this); mInputManager = singletons.getTvInputManagerHelper(); - mSetupUtils = singletons.getSetupUtils(); if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { - mChannelDataManager = singletons.getChannelDataManager(); // Make the channels of the new inputs which have been setup outside Live TV // browsable. if (mChannelDataManager.isDbLoadFinished()) { @@ -148,13 +150,7 @@ public class OnboardingActivity extends SetupActivity { private void showMerchantCollection() { executeActionWithDelay( - new Runnable() { - @Override - public void run() { - startActivity(OnboardingUtils.ONLINE_STORE_INTENT); - } - }, - SHOW_RIPPLE_DURATION_MS); + () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS); } @Override @@ -228,4 +224,11 @@ public class OnboardingActivity extends SetupActivity { } return false; } + + /** Exports {@link OnboardingActivity} for Dagger codegen to create the appropriate injector. */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract OnboardingActivity contributeOnboardingActivityInjector(); + } } diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index f032f622..3566c9c3 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -197,9 +197,13 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { mChannelDataManager.addListener(mChannelDataManagerListener); super.onCreate(savedInstanceState); mParentFragment = (SetupSourcesFragment) getParentFragment(); - singletons - .getTunerInputController() - .executeNetworkTunerDiscoveryAsyncTask(getContext()); + if (singletons.getBuiltInTunerManager().isPresent()) { + singletons + .getBuiltInTunerManager() + .get() + .getTunerInputController() + .executeNetworkTunerDiscoveryAsyncTask(getContext()); + } } @Override @@ -332,7 +336,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { .id(ACTION_ONLINE_STORE) .title(getString(R.string.setup_store_action_title)) .description(getString(R.string.setup_store_action_description)) - .icon(R.drawable.ic_store) + .icon(R.drawable.ic_app_store) .build()); if (newPosition != -1) { diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java index 600aaca1..d85dd50e 100644 --- a/src/com/android/tv/parental/ContentRatingSystem.java +++ b/src/com/android/tv/parental/ContentRatingSystem.java @@ -31,13 +31,10 @@ public class ContentRatingSystem { * A comparator that implements the display order of a group of content rating systems. */ public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR = - new Comparator<ContentRatingSystem>() { - @Override - public int compare(ContentRatingSystem s1, ContentRatingSystem s2) { - String name1 = s1.getDisplayName(); - String name2 = s2.getDisplayName(); - return name1.compareTo(name2); - } + (ContentRatingSystem s1, ContentRatingSystem s2) -> { + String name1 = s1.getDisplayName(); + String name2 = s2.getDisplayName(); + return name1.compareTo(name2); }; private static final String DELIMITER = "/"; diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java index db1f0a4d..b41b160e 100644 --- a/src/com/android/tv/parental/ParentalControlSettings.java +++ b/src/com/android/tv/parental/ParentalControlSettings.java @@ -24,6 +24,7 @@ import com.android.tv.parental.ContentRatingSystem.Rating; import com.android.tv.parental.ContentRatingSystem.SubRating; import com.android.tv.util.TvSettings; import com.android.tv.util.TvSettings.ContentRatingLevel; +import com.google.common.collect.ImmutableList; import java.util.HashSet; import java.util.Set; @@ -160,6 +161,26 @@ public class ParentalControlSettings { } /** + * Checks whether any of given ratings is blocked and returns the first blocked rating. + * + * @param ratings The array of ratings to check + * @return The {@link TvContentRating} that is blocked. + */ + public TvContentRating getBlockedRating(ImmutableList<TvContentRating> ratings) { + if (ratings == null || ratings.isEmpty()) { + return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED) + ? TvContentRating.UNRATED + : null; + } + for (TvContentRating rating : ratings) { + if (mTvInputManager.isRatingBlocked(rating)) { + return rating; + } + } + return null; + } + + /** * Sets the blocked status of a given content rating. * * <p>Note that a call to this method automatically changes the current rating level to {@code @@ -178,34 +199,14 @@ public class ParentalControlSettings { /** * Checks whether any of given ratings is blocked. * - * @param ratings The array of ratings to check + * @param ratings The list of ratings to check * @return {@code true} if a rating is blocked, {@code false} otherwise. */ - public boolean isRatingBlocked(TvContentRating[] ratings) { + public boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) { return getBlockedRating(ratings) != null; } /** - * Checks whether any of given ratings is blocked and returns the first blocked rating. - * - * @param ratings The array of ratings to check - * @return The {@link TvContentRating} that is blocked. - */ - public TvContentRating getBlockedRating(TvContentRating[] ratings) { - if (ratings == null || ratings.length <= 0) { - return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED) - ? TvContentRating.UNRATED - : null; - } - for (TvContentRating rating : ratings) { - if (mTvInputManager.isRatingBlocked(rating)) { - return rating; - } - } - return null; - } - - /** * Checks whether a given rating is blocked by the user or not. * * @param contentRatingSystem The content rating system where the given rating belongs. diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java index 54745f3b..4d21d6d8 100644 --- a/src/com/android/tv/perf/EventNames.java +++ b/src/com/android/tv/perf/EventNames.java @@ -25,31 +25,39 @@ import java.lang.annotation.Retention; * Constants for performance event names. * * <p>Only constants are used to insure no PII is sent. - * + */ public final class EventNames { @Retention(SOURCE) @StringDef({ - APPLICATION_ONCREATE, FETCH_EPG_TASK, - MAIN_ACTIVITY_ONCREATE, - MAIN_ACTIVITY_ONSTART, - MAIN_ACTIVITY_ONRESUME, - ON_DEVICE_SEARCH + ON_DEVICE_SEARCH, + PROGRAM_GUIDE_SHOW, + PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND, + PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE, + PROGRAM_GUIDE_SCROLL_HORIZONTALLY, + PROGRAM_GUIDE_SCROLL_VERTICALLY, + MEMORY_ON_PROGRAM_GUIDE_CLOSE }) public @interface EventName {} - public static final String APPLICATION_ONCREATE = "Application.onCreate"; public static final String FETCH_EPG_TASK = "FetchEpgTask"; - public static final String MAIN_ACTIVITY_ONCREATE = "MainActivity.onCreate"; - public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart"; - public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume"; /** * Event name for query running time of on-device search in {@link * com.android.tv.search.LocalSearchProvider}. */ public static final String ON_DEVICE_SEARCH = "OnDeviceSearch"; + public static final String PROGRAM_GUIDE_SHOW = "ProgramGuide.show"; + public static final String PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND = + "ProgramDataManager.ProgramsPrefetchTask.doInBackground"; + public static final String PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE = + "ProgramGuide.show.fromEmptyCache"; + public static final String PROGRAM_GUIDE_SCROLL_HORIZONTALLY = + "ProgramGuide.scroll.horizontally"; + public static final String PROGRAM_GUIDE_SCROLL_VERTICALLY = "ProgramGuide.scroll.vertically"; + public static final String MEMORY_ON_PROGRAM_GUIDE_CLOSE = "ProgramGuide.memory.close"; + private EventNames() {} } diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java index 111aa851..b1ae759d 100644 --- a/src/com/android/tv/perf/PerformanceMonitor.java +++ b/src/com/android/tv/perf/PerformanceMonitor.java @@ -19,6 +19,7 @@ package com.android.tv.perf; import static com.android.tv.perf.EventNames.EventName; import android.content.Context; +import com.google.errorprone.annotations.CompileTimeConstant; /** Measures Performance. */ public interface PerformanceMonitor { @@ -34,7 +35,7 @@ public interface PerformanceMonitor { * * @param eventName to record */ - void recordMemory(@EventName String eventName); + void recordMemory(@EventName @CompileTimeConstant String eventName); /** * Starts a timer for a global event to allow measuring the event's latency across activities If @@ -42,7 +43,7 @@ public interface PerformanceMonitor { * * @param eventName for which the timer starts */ - void startGlobalTimer(@EventName String eventName); + void startGlobalTimer(@EventName @CompileTimeConstant String eventName); /** * Stops a cross activities timer for a specific eventName and records the timer duration. If no @@ -50,7 +51,7 @@ public interface PerformanceMonitor { * * @param eventName for which the timer stops */ - void stopGlobalTimer(@EventName String eventName); + void stopGlobalTimer(@EventName @CompileTimeConstant String eventName); /** * Starts a timer to record latency of a specific scenario or event. Use this method to track @@ -69,7 +70,7 @@ public interface PerformanceMonitor { * @param event that needs to be stopped * @param eventName for which the timer stops. This must be constant with no PII. */ - void stopTimer(TimerEvent event, @EventName String eventName); + void stopTimer(TimerEvent event, @EventName @CompileTimeConstant String eventName); /** * Starts recording jank for a specific scenario or event. @@ -79,14 +80,14 @@ public interface PerformanceMonitor { * * @param eventName of the event for which tracking is started */ - void startJankRecorder(@EventName String eventName); + void startJankRecorder(@EventName @CompileTimeConstant String eventName); /** * Stops recording jank for a specific event and records the jank event. * * @param eventName of the event that needs to be stopped */ - void stopJankRecorder(@EventName String eventName); + void stopJankRecorder(@EventName @CompileTimeConstant String eventName); /** * Starts activity to display PerformanceMonitor events recorded in local database for debug diff --git a/src/com/android/tv/perf/PerformanceMonitorManager.java b/src/com/android/tv/perf/PerformanceMonitorManager.java new file mode 100644 index 00000000..db6667d1 --- /dev/null +++ b/src/com/android/tv/perf/PerformanceMonitorManager.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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.perf; + +import android.app.Application; + +/** Manages the initialization of Performance Monitoring. */ +public interface PerformanceMonitorManager { + + /** + * Initializes the {@link com.android.tv.perf.PerformanceMonitor}. + * + * <p>This should only be called once. + */ + PerformanceMonitor initialize(Application app); + + /** + * Returns a lightweight object to help measure both cold and warm startup latency. + * + * <p>This method is idempotent and lightweight. It can be called multiple times and does not + * need to be cached. + */ + StartupMeasure getStartupMeasure(); +} diff --git a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java new file mode 100644 index 00000000..fe3ea14b --- /dev/null +++ b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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.perf; + +import com.android.tv.perf.stub.StubPerformanceMonitorManager; +import javax.inject.Inject; + +public final class PerformanceMonitorManagerFactory { + private static final PerformanceMonitorManagerFactory INSTANCE = + new PerformanceMonitorManagerFactory(); + + @Inject + public PerformanceMonitorManagerFactory() {} + + public static PerformanceMonitorManager create() { + return INSTANCE.get(); + } + + public PerformanceMonitorManager get() { + return new StubPerformanceMonitorManager(); + } +} diff --git a/src/com/android/tv/perf/StartupMeasure.java b/src/com/android/tv/perf/StartupMeasure.java new file mode 100644 index 00000000..5cf183ca --- /dev/null +++ b/src/com/android/tv/perf/StartupMeasure.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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.perf; + +import android.app.Activity; +import android.app.Application; + +/** + * Measures App startup. This interface is lightweight to help measure both cold and warm startup + * latency. Implementations must not throw any Exception. + */ +public interface StartupMeasure { + + /** To be be placed as the first static block in the app's Application class. */ + void onAppClassLoaded(); + + /** + * To be placed in your {@link Application#onCreate} to let Performance Monitoring know when + * this happen. + */ + void onAppCreate(Application application); + + /** + * To be placed in an initialization block of your {@link Activity} to let Performance + * Monitoring know when this activity is instantiated. Please note that this initialization + * block should be before other initialization blocks (if any) in your activity class. + */ + void onActivityInit(); +} diff --git a/src/com/android/tv/perf/StubPerformanceMonitor.java b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java index 3742a2a7..80c2f6c5 100644 --- a/src/com/android/tv/perf/StubPerformanceMonitor.java +++ b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java @@ -14,20 +14,17 @@ * limitations under the License. */ -package com.android.tv.perf; +package com.android.tv.perf.stub; -import android.app.Application; import android.content.Context; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.TimerEvent; /** Do nothing implementation of {@link PerformanceMonitor}. */ public final class StubPerformanceMonitor implements PerformanceMonitor { private static final TimerEvent TIMER_EVENT = new TimerEvent() {}; - public static PerformanceMonitor initialize(Application app) { - return new StubPerformanceMonitor(); - } - @Override public void startMemoryMonitor() {} diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java new file mode 100644 index 00000000..0c268155 --- /dev/null +++ b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.perf.stub; + +import android.app.Application; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.PerformanceMonitorManager; +import com.android.tv.perf.StartupMeasure; + +/** Manages a stub implementation of Performance Monitoring. */ +public class StubPerformanceMonitorManager implements PerformanceMonitorManager { + + @Override + public PerformanceMonitor initialize(Application app) { + return new StubPerformanceMonitor(); + } + + @Override + public StartupMeasure getStartupMeasure() { + return new StubStartupMeasure(); + } +} diff --git a/src/com/android/tv/perf/stub/StubStartupMeasure.java b/src/com/android/tv/perf/stub/StubStartupMeasure.java new file mode 100644 index 00000000..d4412261 --- /dev/null +++ b/src/com/android/tv/perf/stub/StubStartupMeasure.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.perf.stub; + +import android.app.Application; +import com.android.tv.perf.StartupMeasure; + +/** Stub implementation of {@link StartupMeasure} */ +public class StubStartupMeasure implements StartupMeasure { + + @Override + public void onAppClassLoaded() {} + + @Override + public void onAppCreate(Application application) {} + + @Override + public void onActivityInit() {} +} diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index d8528bb5..0eb03bec 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -25,10 +25,10 @@ import android.os.Build; import android.util.Log; import com.android.tv.Starter; import com.android.tv.TvActivity; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.dvr.recorder.DvrRecordingService; import com.android.tv.dvr.recorder.RecordingScheduler; +import com.android.tv.features.TvFeatures; import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.OnboardingUtils; @@ -70,7 +70,7 @@ public class BootCompletedReceiver extends BroadcastReceiver { // Grant permission to already set up packages after the system has finished booting. SetupUtils.grantEpgPermissionToSetUpPackages(context); - if (TvFeatures.UNHIDE.isEnabled(context)) { + if (TvFeatures.UNHIDE.isEnabled(context.getApplicationContext())) { if (OnboardingUtils.isFirstBoot(context)) { // Enable the application if this is the first "unhide" feature is enabled just in // case when the app has been disabled before. diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index 07f5d6be..5bc6d724 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -22,8 +22,8 @@ import android.content.Intent; import android.net.Uri; import android.util.Log; import com.android.tv.Starter; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; +import com.android.tv.features.TvFeatures; import com.android.tv.util.Partner; import com.google.android.tv.partner.support.EpgContract; diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java index 410b8252..2590a337 100644 --- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java +++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java @@ -25,9 +25,9 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.RequiresApi; -import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Log; +import androidx.tvprovider.media.tv.TvContractCompat; import com.android.tv.Starter; import com.android.tv.TvSingletons; import com.android.tv.data.PreviewDataManager; @@ -169,18 +169,23 @@ public class ChannelPreviewUpdater { @Override protected Set<Program> doInBackground(Void... params) { Set<Program> programs = new HashSet<>(); - List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels()); - for (Channel channel : channels) { - if (channel.isPhysicalTunerChannel()) { - final Program program = Utils.getCurrentProgram(mContext, channel.getId()); - if (program != null - && isChannelRecommendationApplicable(channel, program)) { - programs.add(program); - if (programs.size() >= RECOMMENDATION_COUNT) { - break; + try { + List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels()); + for (Channel channel : channels) { + if (channel.isPhysicalTunerChannel()) { + final Program program = + Utils.getCurrentProgram(mContext, channel.getId()); + if (program != null + && isChannelRecommendationApplicable(channel, program)) { + programs.add(program); + if (programs.size() >= RECOMMENDATION_COUNT) { + break; + } } } } + } catch (Exception e) { + Log.w(TAG, "Can't update preview data", e); } return programs; } @@ -241,6 +246,17 @@ public class ChannelPreviewUpdater { } } }); + } else if (mJobService != null && mJobParams != null) { + if (DEBUG) { + Log.d( + TAG, + "Preview channel not created because there is only " + + programs.size() + + " programs"); + } + mJobService.jobFinished(mJobParams, false); + mJobService = null; + mJobParams = null; } } else { updatePreviewProgramsForPreviewChannel( diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index 649920fb..fc20031c 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -33,6 +33,7 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; +import android.util.Log; import com.android.tv.TvSingletons; import com.android.tv.common.WeakHandler; import com.android.tv.common.util.PermissionUtils; @@ -52,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap; /** Manages teh data need to make recommendations. */ public class RecommendationDataManager implements WatchedHistoryManager.Listener { + private static final String TAG = "RecommendationDataManag"; private static final int MSG_START = 1000; private static final int MSG_STOP = 1001; private static final int MSG_UPDATE_CHANNELS = 1002; @@ -187,13 +189,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this); mContentObserver = new RecommendationContentObserver(mHandler); mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager(); - runOnMainThread( - new Runnable() { - @Override - public void run() { - start(); - } - }); + runOnMainThread(this::start); } /** @@ -202,13 +198,10 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener */ public void release(@NonNull final Listener listener) { runOnMainThread( - new Runnable() { - @Override - public void run() { - removeListener(listener); - if (mListeners.size() == 0) { - stop(); - } + () -> { + removeListener(listener); + if (mListeners.size() == 0) { + stop(); } }); } @@ -257,13 +250,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } private void addListener(Listener listener) { - runOnMainThread( - new Runnable() { - @Override - public void run() { - mListeners.add(listener); - } - }); + runOnMainThread(() -> mListeners.add(listener)); } @MainThread @@ -347,18 +334,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener history.add(createWatchedProgramFromWatchedProgramCursor(cursor)); } while (cursor.moveToPrevious()); } + } catch (Exception e) { + Log.e(TAG, "Error trying to load watch history from " + uri, e); + return; } for (WatchedProgram watchedProgram : history) { final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(watchedProgram); if (mChannelRecordMapLoaded && channelRecord != null) { runOnMainThread( - new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onNewWatchLog(channelRecord); - } + () -> { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); } }); } @@ -397,12 +384,9 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener convertFromWatchedHistoryManagerRecords(watchedRecord)); if (mChannelRecordMapLoaded && channelRecord != null) { runOnMainThread( - new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onNewWatchLog(channelRecord); - } + () -> { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); } }); } @@ -441,24 +425,18 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private void onNotifyChannelRecordMapLoaded() { mChannelRecordMapLoaded = true; runOnMainThread( - new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onChannelRecordLoaded(); - } + () -> { + for (Listener l : mListeners) { + l.onChannelRecordLoaded(); } }); } private void onNotifyChannelRecordMapChanged() { runOnMainThread( - new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onChannelRecordChanged(); - } + () -> { + for (Listener l : mListeners) { + l.onChannelRecordChanged(); } }); } diff --git a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java deleted file mode 100644 index 528096dd..00000000 --- a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright (C) 2018 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.search; - -import android.support.annotation.Nullable; - -/** - * Hand copy of generated Autovalue class. - * - * TODO get autovalue working - */ - -final class AutoValue_LocalSearchProvider_SearchResult extends LocalSearchProvider.SearchResult { - - private final long channelId; - private final String channelNumber; - private final String title; - private final String description; - private final String imageUri; - private final String intentAction; - private final String intentData; - private final String intentExtraData; - private final String contentType; - private final boolean isLive; - private final int videoWidth; - private final int videoHeight; - private final long duration; - private final int progressPercentage; - - private AutoValue_LocalSearchProvider_SearchResult( - long channelId, - @Nullable String channelNumber, - @Nullable String title, - @Nullable String description, - @Nullable String imageUri, - @Nullable String intentAction, - @Nullable String intentData, - @Nullable String intentExtraData, - @Nullable String contentType, - boolean isLive, - int videoWidth, - int videoHeight, - long duration, - int progressPercentage) { - this.channelId = channelId; - this.channelNumber = channelNumber; - this.title = title; - this.description = description; - this.imageUri = imageUri; - this.intentAction = intentAction; - this.intentData = intentData; - this.intentExtraData = intentExtraData; - this.contentType = contentType; - this.isLive = isLive; - this.videoWidth = videoWidth; - this.videoHeight = videoHeight; - this.duration = duration; - this.progressPercentage = progressPercentage; - } - - @Override - long getChannelId() { - return channelId; - } - - @Nullable - @Override - String getChannelNumber() { - return channelNumber; - } - - @Nullable - @Override - String getTitle() { - return title; - } - - @Nullable - @Override - String getDescription() { - return description; - } - - @Nullable - @Override - String getImageUri() { - return imageUri; - } - - @Nullable - @Override - String getIntentAction() { - return intentAction; - } - - @Nullable - @Override - String getIntentData() { - return intentData; - } - - @Nullable - @Override - String getIntentExtraData() { - return intentExtraData; - } - - @Nullable - @Override - String getContentType() { - return contentType; - } - - @Override - boolean getIsLive() { - return isLive; - } - - @Override - int getVideoWidth() { - return videoWidth; - } - - @Override - int getVideoHeight() { - return videoHeight; - } - - @Override - long getDuration() { - return duration; - } - - @Override - int getProgressPercentage() { - return progressPercentage; - } - - @Override - public String toString() { - return "SearchResult{" - + "channelId=" + channelId + ", " - + "channelNumber=" + channelNumber + ", " - + "title=" + title + ", " - + "description=" + description + ", " - + "imageUri=" + imageUri + ", " - + "intentAction=" + intentAction + ", " - + "intentData=" + intentData + ", " - + "intentExtraData=" + intentExtraData + ", " - + "contentType=" + contentType + ", " - + "isLive=" + isLive + ", " - + "videoWidth=" + videoWidth + ", " - + "videoHeight=" + videoHeight + ", " - + "duration=" + duration + ", " - + "progressPercentage=" + progressPercentage - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof LocalSearchProvider.SearchResult) { - LocalSearchProvider.SearchResult that = (LocalSearchProvider.SearchResult) o; - return (this.channelId == that.getChannelId()) - && ((this.channelNumber == null) ? (that.getChannelNumber() == null) : this.channelNumber.equals(that.getChannelNumber())) - && ((this.title == null) ? (that.getTitle() == null) : this.title.equals(that.getTitle())) - && ((this.description == null) ? (that.getDescription() == null) : this.description.equals(that.getDescription())) - && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri())) - && ((this.intentAction == null) ? (that.getIntentAction() == null) : this.intentAction.equals(that.getIntentAction())) - && ((this.intentData == null) ? (that.getIntentData() == null) : this.intentData.equals(that.getIntentData())) - && ((this.intentExtraData == null) ? (that.getIntentExtraData() == null) : this.intentExtraData.equals(that.getIntentExtraData())) - && ((this.contentType == null) ? (that.getContentType() == null) : this.contentType.equals(that.getContentType())) - && (this.isLive == that.getIsLive()) - && (this.videoWidth == that.getVideoWidth()) - && (this.videoHeight == that.getVideoHeight()) - && (this.duration == that.getDuration()) - && (this.progressPercentage == that.getProgressPercentage()); - } - return false; - } - - @Override - public int hashCode() { - int h$ = 1; - h$ *= 1000003; - h$ ^= (int) ((channelId >>> 32) ^ channelId); - h$ *= 1000003; - h$ ^= (channelNumber == null) ? 0 : channelNumber.hashCode(); - h$ *= 1000003; - h$ ^= (title == null) ? 0 : title.hashCode(); - h$ *= 1000003; - h$ ^= (description == null) ? 0 : description.hashCode(); - h$ *= 1000003; - h$ ^= (imageUri == null) ? 0 : imageUri.hashCode(); - h$ *= 1000003; - h$ ^= (intentAction == null) ? 0 : intentAction.hashCode(); - h$ *= 1000003; - h$ ^= (intentData == null) ? 0 : intentData.hashCode(); - h$ *= 1000003; - h$ ^= (intentExtraData == null) ? 0 : intentExtraData.hashCode(); - h$ *= 1000003; - h$ ^= (contentType == null) ? 0 : contentType.hashCode(); - h$ *= 1000003; - h$ ^= isLive ? 1231 : 1237; - h$ *= 1000003; - h$ ^= videoWidth; - h$ *= 1000003; - h$ ^= videoHeight; - h$ *= 1000003; - h$ ^= (int) ((duration >>> 32) ^ duration); - h$ *= 1000003; - h$ ^= progressPercentage; - return h$; - } - - static final class Builder extends LocalSearchProvider.SearchResult.Builder { - private Long channelId; - private String channelNumber; - private String title; - private String description; - private String imageUri; - private String intentAction; - private String intentData; - private String intentExtraData; - private String contentType; - private Boolean isLive; - private Integer videoWidth; - private Integer videoHeight; - private Long duration; - private Integer progressPercentage; - Builder() { - } - @Override - LocalSearchProvider.SearchResult.Builder setChannelId(long channelId) { - this.channelId = channelId; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setChannelNumber(@Nullable String channelNumber) { - this.channelNumber = channelNumber; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setTitle(@Nullable String title) { - this.title = title; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setDescription(@Nullable String description) { - this.description = description; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setImageUri(@Nullable String imageUri) { - this.imageUri = imageUri; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setIntentAction(@Nullable String intentAction) { - this.intentAction = intentAction; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setIntentData(@Nullable String intentData) { - this.intentData = intentData; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setIntentExtraData(@Nullable String intentExtraData) { - this.intentExtraData = intentExtraData; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setContentType(@Nullable String contentType) { - this.contentType = contentType; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setIsLive(boolean isLive) { - this.isLive = isLive; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setVideoWidth(int videoWidth) { - this.videoWidth = videoWidth; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setVideoHeight(int videoHeight) { - this.videoHeight = videoHeight; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setDuration(long duration) { - this.duration = duration; - return this; - } - @Override - LocalSearchProvider.SearchResult.Builder setProgressPercentage(int progressPercentage) { - this.progressPercentage = progressPercentage; - return this; - } - @Override - LocalSearchProvider.SearchResult build() { - String missing = ""; - if (this.channelId == null) { - missing += " channelId"; - } - if (this.isLive == null) { - missing += " isLive"; - } - if (this.videoWidth == null) { - missing += " videoWidth"; - } - if (this.videoHeight == null) { - missing += " videoHeight"; - } - if (this.duration == null) { - missing += " duration"; - } - if (this.progressPercentage == null) { - missing += " progressPercentage"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_LocalSearchProvider_SearchResult( - this.channelId, - this.channelNumber, - this.title, - this.description, - this.imageUri, - this.intentAction, - this.intentData, - this.intentExtraData, - this.contentType, - this.isLive, - this.videoWidth, - this.videoHeight, - this.duration, - this.progressPercentage); - } - } - -} diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java index 82fb5016..a649c0ac 100644 --- a/src/com/android/tv/search/DataManagerSearch.java +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -34,12 +34,12 @@ import com.android.tv.data.api.Channel; import com.android.tv.search.LocalSearchProvider.SearchResult; import com.android.tv.util.MainThreadExecutor; import com.android.tv.util.Utils; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -68,13 +68,7 @@ public class DataManagerSearch implements SearchInterface { public List<SearchResult> search(final String query, final int limit, final int action) { Future<List<SearchResult>> future = MainThreadExecutor.getInstance() - .submit( - new Callable<List<SearchResult>>() { - @Override - public List<SearchResult> call() throws Exception { - return searchFromDataManagers(query, limit, action); - } - }); + .submit(() -> searchFromDataManagers(query, limit, action)); try { return future.get(); @@ -255,7 +249,7 @@ public class DataManagerSearch implements SearchInterface { result.setIntentData(buildIntentData(channelId)); result.setContentType(Programs.CONTENT_ITEM_TYPE); result.setIsLive(true); - result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE); + result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE); } else { result.setTitle(program.getTitle()); result.setDescription( @@ -299,7 +293,7 @@ public class DataManagerSearch implements SearchInterface { private int getProgressPercentage(long startUtcMillis, long endUtcMillis) { long current = System.currentTimeMillis(); if (startUtcMillis > current || endUtcMillis <= current) { - return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + return SearchInterface.PROGRESS_PERCENTAGE_HIDE; } return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); } @@ -308,10 +302,8 @@ public class DataManagerSearch implements SearchInterface { return TvContract.buildChannelUri(channelId).toString(); } - private boolean isRatingBlocked(TvContentRating[] ratings) { - if (ratings == null - || ratings.length == 0 - || !mTvInputManager.isParentalControlsEnabled()) { + private boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) { + if (ratings == null || ratings.isEmpty() || !mTvInputManager.isParentalControlsEnabled()) { return false; } for (TvContentRating rating : ratings) { diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java index 97e7f229..5652c986 100644 --- a/src/com/android/tv/search/LocalSearchProvider.java +++ b/src/com/android/tv/search/LocalSearchProvider.java @@ -37,6 +37,7 @@ import com.android.tv.perf.EventNames; import com.android.tv.perf.PerformanceMonitor; import com.android.tv.perf.TimerEvent; import com.android.tv.util.TvUriMatcher; +import com.google.auto.value.AutoValue; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -48,8 +49,6 @@ public class LocalSearchProvider extends ContentProvider { /** The authority for LocalSearchProvider. */ public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search"; - public static final int PROGRESS_PERCENTAGE_HIDE = -1; - // TODO: Remove this once added to the SearchManager. private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage"; @@ -223,7 +222,7 @@ public class LocalSearchProvider extends ContentProvider { } /** A placeholder to a search result. */ - // TODO(b/72052568): Get autovalue to work in aosp master + @AutoValue public abstract static class SearchResult { public static Builder builder() { // primitive fields cannot be nullable. Set to default; @@ -236,7 +235,7 @@ public class LocalSearchProvider extends ContentProvider { .setProgressPercentage(0); } - // TODO(b/72052568): Get autovalue to work in aosp master + @AutoValue.Builder abstract static class Builder { abstract Builder setChannelId(long value); diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java index cb26252b..6c94bd33 100644 --- a/src/com/android/tv/search/ProgramGuideSearchFragment.java +++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java @@ -84,7 +84,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { createImageLoaderCallback(cardView)); } else { cardView.setMainImage( - mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96)); + mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96)); } } @@ -171,7 +171,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { View v = super.onCreateView(inflater, container, savedInstanceState); v.setBackgroundResource(R.color.program_guide_scrim); - setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96)); + setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96)); setSearchResultProvider(mSearchResultProvider); setOnItemViewClickedListener(mItemClickedListener); return v; diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java index 4866ee84..d16270ed 100644 --- a/src/com/android/tv/search/SearchInterface.java +++ b/src/com/android/tv/search/SearchInterface.java @@ -26,6 +26,7 @@ public interface SearchInterface { int ACTION_TYPE_SWITCH_CHANNEL = 2; int ACTION_TYPE_SWITCH_INPUT = 3; int ACTION_TYPE_END = 3; + int PROGRESS_PERCENTAGE_HIDE = -1; /** * Search channels, inputs, or programs. This assumes that parental control settings will not be diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index 92197f2d..8a1f51f9 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -36,6 +36,7 @@ import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.util.PermissionUtils; import com.android.tv.search.LocalSearchProvider.SearchResult; import com.android.tv.util.Utils; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -270,7 +271,7 @@ public class TvProviderSearch implements SearchInterface { result.setIntentData(buildIntentData(id)); result.setContentType(Programs.CONTENT_ITEM_TYPE); result.setIsLive(true); - result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE); + result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE); searchResults.add(result.build()); @@ -343,7 +344,7 @@ public class TvProviderSearch implements SearchInterface { private int getProgressPercentage(long startUtcMillis, long endUtcMillis) { long current = System.currentTimeMillis(); if (startUtcMillis > current || endUtcMillis <= current) { - return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + return SearchInterface.PROGRESS_PERCENTAGE_HIDE; } return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); } @@ -481,7 +482,7 @@ public class TvProviderSearch implements SearchInterface { if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) { return false; } - TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings); + ImmutableList<TvContentRating> ratingArray = mTvContentRatingCache.getRatings(ratings); if (ratingArray != null) { for (TvContentRating r : ratingArray) { if (mTvInputManager.isRatingBlocked(r)) { diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java index c6b10e52..b2160b3a 100644 --- a/src/com/android/tv/setup/SystemSetupActivity.java +++ b/src/com/android/tv/setup/SystemSetupActivity.java @@ -64,13 +64,7 @@ public class SystemSetupActivity extends SetupActivity { private void showMerchantCollection() { executeActionWithDelay( - new Runnable() { - @Override - public void run() { - startActivity(OnboardingUtils.ONLINE_STORE_INTENT); - } - }, - SHOW_RIPPLE_DURATION_MS); + () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS); } @Override diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java deleted file mode 100644 index 02611bbf..00000000 --- a/src/com/android/tv/tuner/TunerInputController.java +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright (C) 2015 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.tuner; - -import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.hardware.usb.UsbDevice; -import android.hardware.usb.UsbManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; -import com.android.tv.R; -import com.android.tv.Starter; -import com.android.tv.TvApplication; -import com.android.tv.TvSingletons; -import com.android.tv.common.BuildConfig; -import com.android.tv.common.util.SystemPropertiesProxy; - - -import com.android.tv.tuner.setup.BaseTunerSetupActivity; -import com.android.tv.tuner.util.TunerInputInfoUtils; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Controls the package visibility of {@link BaseTunerTvInputService}. - * - * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code - * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to - * update the connection status of the supported USB TV tuners. - */ -public class TunerInputController { - private static final boolean DEBUG = false; - private static final String TAG = "TunerInputController"; - private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; - private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; - private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; - private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s"; - - /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */ - private static final String CHECKING_NETWORK_TUNER_STATUS = - "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS"; - - private static final String EXTRA_CHECKING_DURATION = - "com.android.tv.action.extra.CHECKING_DURATION"; - private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP"; - - private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); - private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); - private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification"; - - // TODO: Load settings from XML file - private static final TunerDevice[] TUNER_DEVICES = { - new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q - new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q - // WinTV-dualHD (bulk) will be supported after 2017 April security patch. - new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) - new TunerDevice(0x2040, 0x0264, null), - }; - - private static final int MSG_ENABLE_INPUT_SERVICE = 1000; - private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; - - private final ComponentName usbTunerComponent; - private final ComponentName networkTunerComponent; - private final ComponentName builtInTunerComponent; - private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>(); - - private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>(); - private final Map<ComponentName, String> mNotificationMessages = new HashMap<>(); - private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>(); - - private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this); - - public TunerInputController(ComponentName embeddedTuner) { - usbTunerComponent = embeddedTuner; - networkTunerComponent = usbTunerComponent; - builtInTunerComponent = usbTunerComponent; - for (TunerDevice device : TUNER_DEVICES) { - mTunerServiceMapping.put(device, usbTunerComponent); - } - } - - /** Checks status of USB devices to see if there are available USB tuners connected. */ - public void onCheckingUsbTunerStatus(Context context, String action) { - onCheckingUsbTunerStatus(context, action, mHandler); - } - - private void onCheckingUsbTunerStatus( - Context context, String action, @NonNull CheckDvbDeviceHandler handler) { - Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context); - handler.removeMessages(MSG_ENABLE_INPUT_SERVICE); - if (!connectedUsbTuners.isEmpty()) { - // Need to check if DVB driver is accessible. Since the driver creation - // could be happen after the USB event, delay the checking by - // DVB_DRIVER_CHECK_DELAY_MS. - handler.sendMessageDelayed( - handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), - DVB_DRIVER_CHECK_DELAY_MS); - } else { - handleTunerStatusChanged( - context, - false, - connectedUsbTuners, - TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) - ? TunerHal.TUNER_TYPE_USB - : null); - } - } - - private void onNetworkTunerChanged(Context context, boolean enabled) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED) - && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) - == enabled) { - // the status is not changed - return; - } - if (enabled) { - sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); - } else { - sharedPreferences - .edit() - .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) - .apply(); - } - // Network tuner detection is initiated by UI. So the app should not - // be killed. - handleTunerStatusChanged( - context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK); - } - - /** - * See if any USB tuner hardware is attached in the system. - * - * @param context {@link Context} instance - * @return {@code true} if any tuner device we support is plugged in - */ - private Set<TunerDevice> getConnectedUsbTuners(Context context) { - UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - Map<String, UsbDevice> deviceList = manager.getDeviceList(); - String currentSecurityLevel = - SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); - - Set<TunerDevice> devices = new HashSet<>(); - for (UsbDevice device : deviceList.values()) { - if (DEBUG) { - Log.d(TAG, "Device: " + device); - } - for (TunerDevice tuner : TUNER_DEVICES) { - if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) { - Log.i(TAG, "Tuner found"); - devices.add(tuner); - } - } - } - return devices; - } - - private void handleTunerStatusChanged( - Context context, - boolean forceDontKillApp, - Set<TunerDevice> connectedUsbTuners, - Integer triggerType) { - Map<ComponentName, Integer> serviceToEnable = new HashMap<>(); - Set<ComponentName> serviceToDisable = new HashSet<>(); - serviceToDisable.add(builtInTunerComponent); - serviceToDisable.add(networkTunerComponent); - if (TunerFeatures.TUNER.isEnabled(context)) { - // TODO: support both built-in tuner and other tuners at the same time? - if (TunerHal.useBuiltInTuner(context)) { - enableTunerTvInputService( - context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent); - return; - } - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { - serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK); - } - } - for (TunerDevice device : TUNER_DEVICES) { - if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) { - serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB); - } else { - serviceToDisable.add(mTunerServiceMapping.get(device)); - } - } - serviceToDisable.removeAll(serviceToEnable.keySet()); - for (ComponentName serviceComponent : serviceToEnable.keySet()) { - if (isTunerPackageInstalled(context, serviceComponent)) { - enableTunerTvInputService( - context, - true, - forceDontKillApp, - serviceToEnable.get(serviceComponent), - serviceComponent); - } else { - sendNotificationToInstallPackage(context, serviceComponent); - } - } - for (ComponentName serviceComponent : serviceToDisable) { - if (isTunerPackageInstalled(context, serviceComponent)) { - enableTunerTvInputService( - context, false, forceDontKillApp, triggerType, serviceComponent); - } else { - cancelNotificationToInstallPackage(context, serviceComponent); - } - } - } - - /** - * Enable/disable the component {@link BaseTunerTvInputService}. - * - * @param context {@link Context} instance - * @param enabled {@code true} to enable the service; otherwise {@code false} - */ - private static void enableTunerTvInputService( - Context context, - boolean enabled, - boolean forceDontKillApp, - Integer tunerType, - ComponentName serviceComponent) { - if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); - PackageManager pm = context.getPackageManager(); - int newState = - enabled - ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - if (newState != pm.getComponentEnabledSetting(serviceComponent)) { - int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0; - if (serviceComponent.getPackageName().equals(context.getPackageName())) { - // Don't kill APP when handling input count changing. Or the following - // setComponentEnabledSetting() call won't work. - ((TvApplication) context.getApplicationContext()) - .handleInputCountChanged(true, enabled, true); - // Bundled input. Don't kill app if LiveChannels app is active since we don't want - // to kill the running app. - if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) { - flags |= PackageManager.DONT_KILL_APP; - } - // Send/cancel the USB tuner TV input setup notification. - BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); - if (!enabled && tunerType != null) { - if (tunerType == TunerHal.TUNER_TYPE_USB) { - Toast.makeText( - context, - R.string.msg_usb_tuner_disconnected, - Toast.LENGTH_SHORT) - .show(); - } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { - Toast.makeText( - context, - R.string.msg_network_tuner_disconnected, - Toast.LENGTH_SHORT) - .show(); - } - } - } - // Enable/disable the USB tuner TV input. - pm.setComponentEnabledSetting(serviceComponent, newState, flags); - if (DEBUG) Log.d(TAG, "Status updated:" + enabled); - } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) { - // When # of tuners is changed or the tuner input service is switching from/to using - // network tuners or the device just boots. - TunerInputInfoUtils.updateTunerInputInfo(context); - } - } - - /** - * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. - */ - public void executeNetworkTunerDiscoveryAsyncTask(final Context context) { - executeNetworkTunerDiscoveryAsyncTask(context, 0, 0); - } - - /** - * Discovers a network tuner. - * - * @param context {@link Context} - * @param repeatedDurationMs The time length to wait to repeatedly check network status to start - * finding network tuner when the network connection is not available. {@code 0} to disable - * repeatedly checking. - * @param deviceIp The previous discovered device IP, 0 if none. - */ - private void executeNetworkTunerDiscoveryAsyncTask( - final Context context, final long repeatedDurationMs, final int deviceIp) { - if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) { - return; - } - final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); - networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS); - if (!isNetworkConnected(context) && repeatedDurationMs > 0) { - sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs); - } else { - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... params) { - Boolean result = null; - // Implement and execute network tuner discovery AsyncTask here. - return result; - } - - @Override - protected void onPostExecute(Boolean foundNetworkTuner) { - if (foundNetworkTuner == null) { - return; - } - sendCheckingAlarm( - context, - networkCheckingIntent, - foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs); - onNetworkTunerChanged(context, foundNetworkTuner); - } - }.execute(); - } - } - - private static boolean isNetworkConnected(Context context) { - ConnectivityManager cm = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - return networkInfo != null && networkInfo.isConnected(); - } - - private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - intent.putExtra(EXTRA_CHECKING_DURATION, delayMs); - PendingIntent alarmIntent = - PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.set( - AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + delayMs, - alarmIntent); - } - - private static boolean isTunerPackageInstalled( - Context context, ComponentName serviceComponent) { - try { - context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0); - return true; - } catch (NameNotFoundException e) { - return false; - } - } - - private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) { - if (!BuildConfig.ENG) { - return; - } - String applicationName = mTunerApplicationNames.get(serviceComponent); - if (applicationName == null) { - applicationName = context.getString(R.string.tuner_install_default_application_name); - } - String contentTitle = - context.getString( - R.string.tuner_install_notification_content_title, applicationName); - String contentText = mNotificationMessages.get(serviceComponent); - if (contentText == null) { - contentText = context.getString(R.string.tuner_install_notification_content_text); - } - Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent); - if (largeIcon == null) { - // TODO: Make a better default image. - largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store); - } - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) { - createNotificationChannel(context, notificationManager); - } - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData( - Uri.parse( - String.format( - PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName()))); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); - builder.setAutoCancel(true) - .setSmallIcon(R.drawable.ic_launcher_s) - .setLargeIcon(largeIcon) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setCategory(Notification.CATEGORY_RECOMMENDATION) - .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); - notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build()); - } - - private static void cancelNotificationToInstallPackage( - Context context, ComponentName serviceComponent) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(serviceComponent.getPackageName(), 0); - } - - private static void createNotificationChannel( - Context context, NotificationManager notificationManager) { - notificationManager.createNotificationChannel( - new NotificationChannel( - NOTIFICATION_CHANNEL_ID, - context.getResources() - .getString(R.string.ut_setup_notification_channel_name), - NotificationManager.IMPORTANCE_HIGH)); - } - - public static class IntentReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); - Starter.start(context); - TunerInputController tunerInputController = - TvSingletons.getSingletons(context).getTunerInputController(); - if (!TunerFeatures.TUNER.isEnabled(context)) { - tunerInputController.handleTunerStatusChanged( - context, false, Collections.emptySet(), null); - return; - } - switch (intent.getAction()) { - case Intent.ACTION_BOOT_COMPLETED: - tunerInputController.executeNetworkTunerDiscoveryAsyncTask( - context, INITIAL_CHECKING_DURATION_MS, 0); - // fall through - case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: - case UsbManager.ACTION_USB_DEVICE_ATTACHED: - case UsbManager.ACTION_USB_DEVICE_DETACHED: - tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction()); - break; - case CHECKING_NETWORK_TUNER_STATUS: - long repeatedDurationMs = - intent.getLongExtra( - EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS); - tunerInputController.executeNetworkTunerDiscoveryAsyncTask( - context, - Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS), - intent.getIntExtra(EXTRA_DEVICE_IP, 0)); - break; - default: // fall out - } - } - } - - /** - * Simple data holder for a USB device. Used to represent a tuner model, and compare against - * {@link UsbDevice}. - */ - private static class TunerDevice { - private final int vendorId; - private final int productId; - - // security patch level from which the specific tuner type is supported. - private final String minSecurityLevel; - - private TunerDevice(int vendorId, int productId, String minSecurityLevel) { - this.vendorId = vendorId; - this.productId = productId; - this.minSecurityLevel = minSecurityLevel; - } - - private boolean equalsTo(UsbDevice device) { - return device.getVendorId() == vendorId && device.getProductId() == productId; - } - - private boolean isSupported(String currentSecurityLevel) { - if (minSecurityLevel == null) { - return true; - } - - long supportSecurityLevelTimeStamp = 0; - long currentSecurityLevelTimestamp = 0; - try { - SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT); - supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime(); - currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime(); - } catch (ParseException e) { - } - return supportSecurityLevelTimeStamp != 0 - && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp; - } - } - - private static class CheckDvbDeviceHandler extends Handler { - - private final TunerInputController mTunerInputController; - private DvbDeviceAccessor mDvbDeviceAccessor; - - CheckDvbDeviceHandler(TunerInputController tunerInputController) { - super(Looper.getMainLooper()); - this.mTunerInputController = tunerInputController; - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_ENABLE_INPUT_SERVICE: - Context context = (Context) msg.obj; - if (mDvbDeviceAccessor == null) { - mDvbDeviceAccessor = new DvbDeviceAccessor(context); - } - boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); - mTunerInputController.handleTunerStatusChanged( - context, - false, - enabled - ? mTunerInputController.getConnectedUsbTuners(context) - : Collections.emptySet(), - TunerHal.TUNER_TYPE_USB); - break; - default: // fall out - } - } - } -} diff --git a/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java new file mode 100644 index 00000000..e4fa35d9 --- /dev/null +++ b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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.tunerinputcontroller; + +import com.android.tv.common.singletons.HasTvInputId; + +/** Controllers and parameters needed to access a built in tuner. */ +public interface BuiltInTunerManager extends HasTvInputId { + TunerInputController getTunerInputController(); +} diff --git a/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java new file mode 100644 index 00000000..90540bc3 --- /dev/null +++ b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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.tunerinputcontroller; + +import com.google.common.base.Optional; + +/** + * Has optional {@link BuiltInTunerManager}. + * + * <p>If the {@code BuiltInTunerManager} is absent the built tuner is not enabled. + */ +public interface HasBuiltInTunerManager { + + /** @deprecated inject instead */ + @Deprecated + Optional<BuiltInTunerManager> getBuiltInTunerManager(); +} diff --git a/src/com/android/tv/tunerinputcontroller/TunerInputController.java b/src/com/android/tv/tunerinputcontroller/TunerInputController.java new file mode 100644 index 00000000..f822dbe0 --- /dev/null +++ b/src/com/android/tv/tunerinputcontroller/TunerInputController.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.tunerinputcontroller; + +import android.content.Context; +import android.content.Intent; + +/** Controls the package visibility of built in tuner services. */ +public interface TunerInputController { + + Intent createSetupIntent(Context context); + + void onCheckingUsbTunerStatus(Context context, String action); + + void executeNetworkTunerDiscoveryAsyncTask(Context context); + + /** + * Updates tuner input's info. + * + * @param context {@link Context} instance + */ + void updateTunerInputInfo(Context context); +} diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index b2be9f02..e2b64a1e 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -17,10 +17,10 @@ package com.android.tv.ui; import android.content.Context; -import android.media.tv.TvView; import android.util.AttributeSet; import android.view.SurfaceView; import android.view.View; +import com.android.tv.common.compat.TvViewCompat; import com.android.tv.common.util.CommonUtils; import com.android.tv.common.util.Debug; @@ -31,7 +31,7 @@ import com.android.tv.common.util.Debug; * android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app * uses only application layer. TODO: remove this class once the TvView.setMain() is revisited. */ -public class AppLayerTvView extends TvView { +public class AppLayerTvView extends TvViewCompat { public AppLayerTvView(Context context) { super(context); } diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java index 6b2d9a01..b7a2dd95 100644 --- a/src/com/android/tv/ui/BlockScreenView.java +++ b/src/com/android/tv/ui/BlockScreenView.java @@ -22,6 +22,7 @@ import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -180,6 +181,10 @@ public class BlockScreenView extends FrameLayout { requestLayout(); } + public void setInfoTextOnClickListener(@Nullable OnClickListener onClickListener) { + mBlockingInfoTextView.setOnClickListener(onClickListener); + } + /** Changes the view layout according to the {@code blockScreenType}. */ public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) { if (!withAnimation) { @@ -252,4 +257,8 @@ public class BlockScreenView extends FrameLayout { mInfoFadeOut.end(); } } + + public void setInfoTextClickable(boolean clickable) { + mBlockingInfoTextView.setClickable(clickable); + } } diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index 28325197..00ac7e32 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -46,11 +46,10 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; -import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; import com.android.tv.data.api.Channel; @@ -59,11 +58,14 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.ui.TvTransitionManager.TransitionLayout; import com.android.tv.ui.hideable.AutoHideScheduler; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import com.android.tv.util.images.ImageCache; import com.android.tv.util.images.ImageLoader; import com.android.tv.util.images.ImageLoader.ImageLoaderCallback; import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask; +import com.google.common.collect.ImmutableList; +import javax.inject.Provider; /** A view to render channel banner. */ public class ChannelBannerView extends FrameLayout @@ -74,6 +76,21 @@ public class ChannelBannerView extends FrameLayout /** Show all information at the channel banner. */ public static final int LOCK_NONE = 0; + /** Singletons needed for this class. */ + public interface MySingletons { + Provider<Channel> getCurrentChannelProvider(); + + Provider<Program> getCurrentProgramProvider(); + + Provider<TvOverlayManager> getOverlayManagerProvider(); + + TvInputManagerHelper getTvInputManagerHelperSingleton(); + + Provider<Long> getCurrentPlayingPositionProvider(); + + DvrManager getDvrManagerSingleton(); + } + /** * Lock program details at the channel banner. This is used when a content is locked so we don't * want to show program details including program description text and poster art. @@ -94,14 +111,21 @@ public class ChannelBannerView extends FrameLayout private Program mLockedChannelProgram; private static String sClosedCaptionMark; - private final MainActivity mMainActivity; private final Resources mResources; + private final Provider<Channel> mCurrentChannelProvider; + private final Provider<Program> mCurrentProgramProvider; + private final Provider<Long> mCurrentPlayingPositionProvider; + private final TvInputManagerHelper mTvInputManagerHelper; + // TvOverlayManager is always created after ChannelBannerView + private final Provider<TvOverlayManager> mTvOverlayManager; + private View mChannelView; private TextView mChannelNumberTextView; private ImageView mChannelLogoImageView; private TextView mProgramTextView; private ImageView mTvInputLogoImageView; + private ImageView mChannelSignalStrengthView; private TextView mChannelNameTextView; private TextView mProgramTimeTextView; private ProgressBar mRemainingTimeView; @@ -143,6 +167,32 @@ public class ChannelBannerView extends FrameLayout private final int mRecordingIconPadding; private final Interpolator mResizeInterpolator; + /** + * 0 - 100 represent signal strength percentage. Strength is divided into 5 levels (0 - 4). + * + * <p>This is the upper boundary of level 0 [0%, 20%], and the lower boundary of level 1 (20%, + * 40%]. + */ + private static final int SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND = 20; + + /** + * This is the upper boundary of level 1 (20%, 40%], and the lower boundary of level 2 (40%, + * 60%]. + */ + private static final int SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND = 40; + + /** + * This is the upper boundary of level of level 2. (40%, 60%], and the lower boundary of level 3 + * (60%, 80%]. + */ + private static final int SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND = 60; + + /** + * This is the upper boundary of level of level 3 (60%, 80%], and the lower boundary of level 4 + * (80%, 100%]. + */ + private static final int SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND = 80; + private final AnimatorListenerAdapter mResizeAnimatorListener = new AnimatorListenerAdapter() { @Override @@ -172,7 +222,14 @@ public class ChannelBannerView extends FrameLayout public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mResources = getResources(); - mMainActivity = (MainActivity) context; + + @SuppressWarnings("unchecked") // injection + MySingletons singletons = HasSingletons.get(MySingletons.class, context); + mCurrentChannelProvider = singletons.getCurrentChannelProvider(); + mCurrentProgramProvider = singletons.getCurrentProgramProvider(); + mCurrentPlayingPositionProvider = singletons.getCurrentPlayingPositionProvider(); + mTvInputManagerHelper = singletons.getTvInputManagerHelperSingleton(); + mTvOverlayManager = singletons.getOverlayManagerProvider(); mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration); mChannelLogoImageViewWidth = @@ -195,20 +252,17 @@ public class ChannelBannerView extends FrameLayout mProgramDescriptionFadeInAnimator = AnimatorInflater.loadAnimator( - mMainActivity, R.animator.channel_banner_program_description_fade_in); + context, R.animator.channel_banner_program_description_fade_in); mProgramDescriptionFadeOutAnimator = AnimatorInflater.loadAnimator( - mMainActivity, R.animator.channel_banner_program_description_fade_out); + context, R.animator.channel_banner_program_description_fade_out); - if (CommonFeatures.DVR.isEnabled(mMainActivity)) { - mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager(); + if (CommonFeatures.DVR.isEnabled(context)) { + mDvrManager = singletons.getDvrManagerSingleton(); } else { mDvrManager = null; } - mContentRatingsManager = - TvSingletons.getSingletons(getContext()) - .getTvInputManagerHelper() - .getContentRatingsManager(); + mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager(); mNoProgram = new Program.Builder() @@ -234,22 +288,23 @@ public class ChannelBannerView extends FrameLayout mChannelView = findViewById(R.id.channel_banner_view); - mChannelNumberTextView = (TextView) findViewById(R.id.channel_number); - mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo); - mProgramTextView = (TextView) findViewById(R.id.program_text); - mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo); - mChannelNameTextView = (TextView) findViewById(R.id.channel_name); - mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text); - mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time); - mRecordingIndicatorView = (TextView) findViewById(R.id.recording_indicator); - mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption); - mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio); - mResolutionTextView = (TextView) findViewById(R.id.resolution); - mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel); - mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0); - mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1); - mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2); - mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description); + mChannelNumberTextView = findViewById(R.id.channel_number); + mChannelLogoImageView = findViewById(R.id.channel_logo); + mProgramTextView = findViewById(R.id.program_text); + mTvInputLogoImageView = findViewById(R.id.tvinput_logo); + mChannelSignalStrengthView = findViewById(R.id.channel_signal_strength); + mChannelNameTextView = findViewById(R.id.channel_name); + mProgramTimeTextView = findViewById(R.id.program_time_text); + mRemainingTimeView = findViewById(R.id.remaining_time); + mRecordingIndicatorView = findViewById(R.id.recording_indicator); + mClosedCaptionTextView = findViewById(R.id.closed_caption); + mAspectRatioTextView = findViewById(R.id.aspect_ratio); + mResolutionTextView = findViewById(R.id.resolution); + mAudioChannelTextView = findViewById(R.id.audio_channel); + mContentRatingsTextViews[0] = findViewById(R.id.content_ratings_0); + mContentRatingsTextViews[1] = findViewById(R.id.content_ratings_1); + mContentRatingsTextViews[2] = findViewById(R.id.content_ratings_2); + mProgramDescriptionTextView = findViewById(R.id.program_description); mAnchorView = findViewById(R.id.anchor); mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView); @@ -310,7 +365,7 @@ public class ChannelBannerView extends FrameLayout */ public void setBlockingContentRating(TvContentRating rating) { mBlockingContentRating = rating; - updateProgramRatings(mMainActivity.getCurrentProgram()); + updateProgramRatings(mCurrentProgramProvider.get()); } /** @@ -328,20 +383,20 @@ public class ChannelBannerView extends FrameLayout mAutoHideScheduler.schedule(mShowDurationMillis); } mBlockingContentRating = null; - mCurrentChannel = mMainActivity.getCurrentChannel(); + mCurrentChannel = mCurrentChannelProvider.get(); mCurrentChannelLogoExists = mCurrentChannel != null && mCurrentChannel.channelLogoExists(); updateStreamInfo(null); updateChannelInfo(); } - updateProgramInfo(mMainActivity.getCurrentProgram()); + updateProgramInfo(mCurrentProgramProvider.get()); mUpdateOnTune = false; } private void hide() { mCurrentHeight = 0; - mMainActivity - .getOverlayManager() + mTvOverlayManager + .get() .hideOverlays( TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS @@ -367,10 +422,10 @@ public class ChannelBannerView extends FrameLayout updateText( mResolutionTextView, Utils.getVideoDefinitionLevelString( - mMainActivity, info.getVideoDefinitionLevel())); + getContext(), info.getVideoDefinitionLevel())); updateText( mAudioChannelTextView, - Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount())); + Utils.getAudioChannelString(getContext(), info.getAudioChannelCount())); } else { // Channel change has been requested. But, StreamInfo hasn't been updated yet. mClosedCaptionTextView.setVisibility(View.GONE); @@ -418,8 +473,7 @@ public class ChannelBannerView extends FrameLayout } mChannelNumberTextView.setText(displayNumber); mChannelNameTextView.setText(displayName); - TvInputInfo info = - mMainActivity.getTvInputManagerHelper().getTvInputInfo(getCurrentInputId()); + TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(getCurrentInputId()); if (info == null || !ImageLoader.loadBitmap( createTvInputLogoLoaderCallback(info, this), @@ -440,7 +494,7 @@ public class ChannelBannerView extends FrameLayout } private String getCurrentInputId() { - Channel channel = mMainActivity.getCurrentChannel(); + Channel channel = mCurrentChannelProvider.get(); return channel != null ? channel.getInputId() : null; } @@ -503,6 +557,34 @@ public class ChannelBannerView extends FrameLayout }; } + public void updateChannelSignalStrengthView(int value) { + int resId = signalStrenghtToResId(value); + if (resId != 0) { + mChannelSignalStrengthView.setVisibility(View.VISIBLE); + mChannelSignalStrengthView.setImageResource(resId); + } else { + mChannelSignalStrengthView.setVisibility(View.GONE); + } + } + + private int signalStrenghtToResId(int value) { + int signal = 0; + if (value >= 0 && value <= 100) { + if (value <= SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND) { + signal = R.drawable.quantum_ic_signal_cellular_0_bar_white_24; + } else if (value <= SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND) { + signal = R.drawable.quantum_ic_signal_cellular_1_bar_white_24; + } else if (value <= SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND) { + signal = R.drawable.quantum_ic_signal_cellular_2_bar_white_24; + } else if (value <= SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND) { + signal = R.drawable.quantum_ic_signal_cellular_3_bar_white_24; + } else { + signal = R.drawable.quantum_ic_signal_cellular_4_bar_white_24; + } + } + return signal; + } + private void updateLogo(@Nullable Bitmap logo) { if (logo == null) { // Need to update the text size of the program text view depending on the channel logo. @@ -651,13 +733,14 @@ public class ChannelBannerView extends FrameLayout mContentRatingsTextViews[i].setVisibility(View.GONE); } } else { - TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); + ImmutableList<TvContentRating> ratings = + (program == null) ? null : program.getContentRatings(); for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { - if (ratings == null || ratings.length <= i) { + if (ratings == null || ratings.size() <= i) { mContentRatingsTextViews[i].setVisibility(View.GONE); } else { mContentRatingsTextViews[i].setText( - mContentRatingsManager.getDisplayNameForRating(ratings[i])); + mContentRatingsManager.getDisplayNameForRating(ratings.get(i))); mContentRatingsTextViews[i].setVisibility(View.VISIBLE); } } @@ -667,13 +750,11 @@ public class ChannelBannerView extends FrameLayout private void updateProgramTimeInfo(Program program) { long durationMs = program.getDurationMillis(); long startTimeMs = program.getStartTimeUtcMillis(); - long endTimeMs = program.getEndTimeUtcMillis(); if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) { mProgramTimeTextView.setVisibility(View.VISIBLE); mRemainingTimeView.setVisibility(View.VISIBLE); - mProgramTimeTextView.setText( - Utils.getDurationString(getContext(), startTimeMs, endTimeMs, true)); + mProgramTimeTextView.setText(program.getDurationString(getContext())); } else { mProgramTimeTextView.setVisibility(View.GONE); mRemainingTimeView.setVisibility(View.GONE); @@ -713,7 +794,7 @@ public class ChannelBannerView extends FrameLayout Program program, @Nullable ScheduledRecording recording) { long programStartTime = program.getStartTimeUtcMillis(); long programEndTime = program.getEndTimeUtcMillis(); - long currentPosition = mMainActivity.getCurrentPlayingPosition(); + long currentPosition = mCurrentPlayingPositionProvider.get(); updateRecordingIndicator(recording); if (recording != null) { // Recording now. Use recording-style progress bar. @@ -734,12 +815,12 @@ public class ChannelBannerView extends FrameLayout if (recording != null) { if (mRemainingTimeView.getVisibility() == View.GONE) { mRecordingIndicatorView.setText( - mMainActivity + getContext() .getResources() .getString( R.string.dvr_recording_till_format, DateUtils.formatDateTime( - mMainActivity, + getContext(), recording.getEndTimeMs(), DateUtils.FORMAT_SHOW_TIME))); mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding); @@ -754,7 +835,7 @@ public class ChannelBannerView extends FrameLayout } private boolean isCurrentProgram(ScheduledRecording recording, Program program) { - long currentPosition = mMainActivity.getCurrentPlayingPosition(); + long currentPosition = mCurrentPlayingPositionProvider.get(); return (recording.getType() == ScheduledRecording.TYPE_PROGRAM && recording.getProgramId() == program.getId()) || (recording.getType() == ScheduledRecording.TYPE_TIMED diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java new file mode 100644 index 00000000..80c0f64b --- /dev/null +++ b/src/com/android/tv/ui/DetailsActivity.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2016 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; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.v17.leanback.app.DetailsFragment; +import android.transition.Transition; +import android.transition.Transition.TransitionListener; +import android.util.Log; +import android.view.View; +import com.android.tv.R; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ui.browse.CurrentRecordingDetailsFragment; +import com.android.tv.dvr.ui.browse.RecordedProgramDetailsFragment; +import com.android.tv.dvr.ui.browse.ScheduledRecordingDetailsFragment; +import com.android.tv.dvr.ui.browse.SeriesRecordingDetailsFragment; + +/** Activity to show details view. */ +public class DetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener { + private static final String TAG = "DetailsActivity"; + + private static final long INVALID_RECORD_ID = -1; + + /** Name of record id added to the Intent. */ + public static final String RECORDING_ID = "record_id"; + /** Name of program uri added to the Intent. */ + public static final String PROGRAM = "program"; + /** Name of channel id added to the Intent. */ + public static final String CHANNEL_ID = "channel_id"; + /** Name of input id added to the Intent. */ + public static final String INPUT_ID = "input_id"; + + /** + * Name of flag added to the Intent to determine if details view should hide "View schedule" + * button. + */ + public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; + + /** Name of details view's type added to the intent. */ + public static final String DETAILS_VIEW_TYPE = "details_view_type"; + + /** Name of shared element between activities. */ + public static final String SHARED_ELEMENT_NAME = "shared_element"; + + /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */ + public static final int CURRENT_RECORDING_VIEW = 1; + + /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */ + public static final int SCHEDULED_RECORDING_VIEW = 2; + + /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */ + public static final int RECORDED_PROGRAM_VIEW = 3; + + /** SERIES_RECORDING_VIEW refers to series recording in DVR. */ + public static final int SERIES_RECORDING_VIEW = 4; + + /** SERIES_RECORDING_VIEW refers to program. */ + public static final int PROGRAM_VIEW = 5; + + public static final int REQUEST_DELETE = 1; + + private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener; + private long mRecordId = INVALID_RECORD_ID; + + @Override + public void onCreate(Bundle savedInstanceState) { + Starter.start(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_details); + long recordId = getIntent().getLongExtra(RECORDING_ID, INVALID_RECORD_ID); + int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); + boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); + long channelId = getIntent().getLongExtra(CHANNEL_ID, -1); + DetailsFragment detailsFragment = null; + Bundle args = new Bundle(); + if (detailsViewType != -1 && savedInstanceState == null) { + if (recordId != INVALID_RECORD_ID) { + mRecordId = recordId; + args.putLong(RECORDING_ID, mRecordId); + if (detailsViewType == CURRENT_RECORDING_VIEW) { + detailsFragment = new CurrentRecordingDetailsFragment(); + } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { + args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); + detailsFragment = new ScheduledRecordingDetailsFragment(); + } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { + detailsFragment = new RecordedProgramDetailsFragment(); + } else if (detailsViewType == SERIES_RECORDING_VIEW) { + detailsFragment = new SeriesRecordingDetailsFragment(); + } + } else if (detailsViewType == PROGRAM_VIEW && channelId != -1) { + Parcelable program = getIntent().getParcelableExtra(PROGRAM); + if (program != null) { + args.putLong(CHANNEL_ID, channelId); + args.putParcelable(PROGRAM, program); + args.putString(INPUT_ID, getIntent().getStringExtra(INPUT_ID)); + detailsFragment = new ProgramDetailsFragment(); + } + } + if (detailsFragment != null) { + detailsFragment.setArguments(args); + getFragmentManager() + .beginTransaction() + .replace(R.id.dvr_details_view_frame, detailsFragment) + .commit(); + } + } + + // This is a workaround for the focus on O device + addTransitionListener(); + } + + @Override + public void onPinChecked(boolean checked, int type, String rating) { + if (mOnPinCheckedListener != null) { + mOnPinCheckedListener.onPinChecked(checked, type, rating); + } + } + + public void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) { + mOnPinCheckedListener = listener; + } + + private void addTransitionListener() { + getWindow() + .getSharedElementEnterTransition() + .addListener( + new TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + // Do nothing + } + + @Override + public void onTransitionEnd(Transition transition) { + View actions = findViewById(R.id.details_overview_actions); + if (actions != null) { + actions.requestFocus(); + } + } + + @Override + public void onTransitionCancel(Transition transition) { + // Do nothing + + } + + @Override + public void onTransitionPause(Transition transition) { + // Do nothing + } + + @Override + public void onTransitionResume(Transition transition) { + // Do nothing + } + }); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_DELETE: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + delete(true); + } else { + Log.i( + TAG, + "Write permission denied, Not trying to delete the file for " + + mRecordId); + delete(false); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void delete(boolean deleteFile) { + if (mRecordId != INVALID_RECORD_ID) { + DvrManager dvrManager = TvSingletons.getSingletons(this).getDvrManager(); + dvrManager.removeRecordedProgram(mRecordId, deleteFile); + } + finish(); + } +} diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java index 800fa85a..d3fec824 100644 --- a/src/com/android/tv/ui/FullscreenDialogView.java +++ b/src/com/android/tv/ui/FullscreenDialogView.java @@ -83,13 +83,7 @@ public class FullscreenDialogView extends FrameLayout /** Dismisses the host {@link Dialog}. */ protected void dismiss() { - startExitAnimation( - new Runnable() { - @Override - public void run() { - mDialog.dismiss(); - } - }); + startExitAnimation(() -> mDialog.dismiss()); } @Override @@ -110,9 +104,7 @@ public class FullscreenDialogView extends FrameLayout v.mSkipEnterAlphaAnimation = true; v.initialize(mActivity, mDialog); startExitAnimation( - new Runnable() { - @Override - public void run() { + () -> new Handler() .postDelayed( new Runnable() { @@ -122,9 +114,7 @@ public class FullscreenDialogView extends FrameLayout getDialog().setContentView(v); } }, - TRANSITION_INTERVAL_MS); - } - }); + TRANSITION_INTERVAL_MS)); } /** Called when an enter animation starts. Sub-view specific animation can be implemented. */ diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java index 5ac715bf..d0609186 100644 --- a/src/com/android/tv/ui/InputBannerView.java +++ b/src/com/android/tv/ui/InputBannerView.java @@ -31,9 +31,7 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager private final long mShowDurationMillis; private final Runnable mHideRunnable = - new Runnable() { - @Override - public void run() { + () -> ((MainActivity) getContext()) .getOverlayManager() .hideOverlays( @@ -42,9 +40,6 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); - } - }; - private TextView mInputLabelTextView; private TextView mSecondaryInputLabelTextView; diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java index be9fb691..e7240747 100644 --- a/src/com/android/tv/ui/IntroView.java +++ b/src/com/android/tv/ui/IntroView.java @@ -102,13 +102,7 @@ public class IntroView extends FullscreenDialogView { .setInterpolator(interpolator) .setDuration(duration) .withLayer() - .withEndAction( - new Runnable() { - @Override - public void run() { - onAnimationEnded.run(); - } - }) + .withEndAction(onAnimationEnded) .start(); } } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index e2625811..a26175a4 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -148,13 +148,10 @@ public class KeypadChannelSwitchView extends LinearLayout mChannelItemListView.setFocusable(false); final Channel channel = ((Channel) mAdapter.getItem(position)); postDelayed( - new Runnable() { - @Override - public void run() { - mChannelItemListView.setFocusable(true); - mMainActivity.tuneToChannel(channel); - mTracker.sendChannelNumberItemClicked(); - } + () -> { + mChannelItemListView.setFocusable(true); + mMainActivity.tuneToChannel(channel); + mTracker.sendChannelNumberItemClicked(); }, mRippleAnimDurationMillis); } diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java new file mode 100644 index 00000000..88a7b2ca --- /dev/null +++ b/src/com/android/tv/ui/ProgramDetailsFragment.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2018 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; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.DetailsOverviewRow; +import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PresenterSelector; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.support.v17.leanback.widget.VerticalGridView; +import android.text.TextUtils; +import com.android.tv.R; +import com.android.tv.TvSingletons; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.browse.ActionPresenterSelector; +import com.android.tv.dvr.ui.browse.DetailsContent; +import com.android.tv.dvr.ui.browse.DetailsContentPresenter; +import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper; +import com.android.tv.util.images.ImageLoader; + +/** A fragment shows the details of a Program */ +public class ProgramDetailsFragment extends DetailsFragment + implements DvrDataManager.ScheduledRecordingListener, + DvrScheduleManager.OnConflictStateChangeListener { + private static final int LOAD_LOGO_IMAGE = 1; + private static final int LOAD_BACKGROUND_IMAGE = 2; + + private static final int ACTION_VIEW_SCHEDULE = 1; + private static final int ACTION_CANCEL = 2; + private static final int ACTION_SCHEDULE_RECORDING = 3; + + protected DetailsViewBackgroundHelper mBackgroundHelper; + private ArrayObjectAdapter mRowsAdapter; + private DetailsOverviewRow mDetailsOverview; + private Program mProgram; + private String mInputId; + private ScheduledRecording mScheduledRecording; + private DvrManager mDvrManager; + private DvrDataManager mDvrDataManager; + private DvrScheduleManager mDvrScheduleManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!onLoadDetails(getArguments())) { + getActivity().finish(); + } + } + + @Override + public void onDestroy() { + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrScheduleManager.removeOnConflictStateChangeListener(this); + super.onDestroy(); + } + + @Override + public void onStart() { + super.onStart(); + VerticalGridView container = + (VerticalGridView) getActivity().findViewById(R.id.container_list); + // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. + container.setItemAlignmentOffset(0); + container.setWindowAlignmentOffset( + getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); + } + + private void setupAdapter() { + DetailsOverviewRowPresenter rowPresenter = + new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity())); + rowPresenter.setBackgroundColor( + getResources().getColor(R.color.common_tv_background, null)); + rowPresenter.setSharedElementEnterTransition( + getActivity(), DetailsActivity.SHARED_ELEMENT_NAME); + rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); + mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); + setAdapter(mRowsAdapter); + } + + /** Sets details overview. */ + protected void setDetailsOverviewRow(DetailsContent detailsContent) { + mDetailsOverview = new DetailsOverviewRow(detailsContent); + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + mRowsAdapter.add(mDetailsOverview); + onLoadLogoAndBackgroundImages(detailsContent); + } + + /** Creates and returns presenter selector will be used by rows adaptor. */ + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); + return presenterSelector; + } + + /** Updates actions of details overview. */ + protected void updateActions() { + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + } + + /** + * Loads program details according to the arguments the fragment got. + * + * @return false if cannot find valid programs, else return true. If the return value is false, + * the detail activity and fragment will be ended. + */ + private boolean onLoadDetails(Bundle args) { + Program program = args.getParcelable(DetailsActivity.PROGRAM); + long channelId = args.getLong(DetailsActivity.CHANNEL_ID); + String inputId = args.getString(DetailsActivity.INPUT_ID); + if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) { + mProgram = program; + mInputId = inputId; + TvSingletons singletons = TvSingletons.getSingletons(getContext()); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrManager = singletons.getDvrManager(); + mDvrScheduleManager = singletons.getDvrScheduleManager(); + mScheduledRecording = + mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); + mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); + setupAdapter(); + setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram)); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrScheduleManager.addOnConflictStateChangeListener(this); + return true; + } + return false; + } + + private int getScheduleIconId() { + if (mDvrManager.isConflicting(mScheduledRecording)) { + return R.drawable.ic_warning_white_32dp; + } else { + return R.drawable.ic_schedule_32dp; + } + } + + /** Creates actions users can interact with and their adaptor for this fragment. */ + private SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + if (mScheduledRecording != null) { + adapter.set( + ACTION_VIEW_SCHEDULE, + new Action( + ACTION_VIEW_SCHEDULE, + res.getString(R.string.dvr_detail_view_schedule), + null, + res.getDrawable(getScheduleIconId()))); + adapter.set( + ACTION_CANCEL, + new Action( + ACTION_CANCEL, + res.getString(R.string.dvr_detail_cancel_recording), + null, + res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); + } else if (CommonFeatures.DVR.isEnabled(getActivity()) + && mDvrManager.isProgramRecordable(mProgram)) { + adapter.set( + ACTION_SCHEDULE_RECORDING, + new Action( + ACTION_SCHEDULE_RECORDING, + res.getString(R.string.dvr_detail_schedule_recording), + null, + res.getDrawable(R.drawable.ic_schedule_32dp))); + } + return adapter; + } + + /** + * Creates actions listeners to implement the behavior of the fragment after users click some + * action buttons. + */ + private OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + long actionId = action.getId(); + if (actionId == ACTION_VIEW_SCHEDULE) { + DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording); + } else if (actionId == ACTION_CANCEL) { + mDvrManager.removeScheduledRecording(mScheduledRecording); + } else if (actionId == ACTION_SCHEDULE_RECORDING) { + DvrUiHelper.checkStorageStatusAndShowErrorMessage( + getActivity(), + mInputId, + () -> + DvrUiHelper.requestRecordingFutureProgram( + getActivity(), mProgram, false)); + } + } + }; + } + + /** Loads logo and background images for detail fragments. */ + protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { + Drawable logoDrawable = null; + Drawable backgroundDrawable = null; + if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { + logoDrawable = + getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); + mDetailsOverview.setImageDrawable(logoDrawable); + } + if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { + backgroundDrawable = + getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); + mBackgroundHelper.setBackground(backgroundDrawable); + } + if (logoDrawable != null && backgroundDrawable != null) { + return; + } + if (logoDrawable == null + && backgroundDrawable == null + && detailsContent + .getLogoImageUri() + .equals(detailsContent.getBackgroundImageUri())) { + ImageLoader.loadBitmap( + getContext(), + detailsContent.getLogoImageUri(), + new MyImageLoaderCallback( + this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext())); + return; + } + if (logoDrawable == null) { + int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); + int imageHeight = + getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height); + ImageLoader.loadBitmap( + getContext(), + detailsContent.getLogoImageUri(), + imageWidth, + imageHeight, + new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); + } + if (backgroundDrawable == null) { + ImageLoader.loadBitmap( + getContext(), + detailsContent.getBackgroundImageUri(), + new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); + } + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording recording : scheduledRecordings) { + if (recording.getProgramId() == mProgram.getId()) { + mScheduledRecording = recording; + updateActions(); + return; + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + if (mScheduledRecording == null) { + return; + } + for (ScheduledRecording recording : scheduledRecordings) { + if (recording.getId() == mScheduledRecording.getId()) { + mScheduledRecording = null; + updateActions(); + return; + } + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + if (mScheduledRecording == null) { + return; + } + for (ScheduledRecording recording : scheduledRecordings) { + if (recording.getId() == mScheduledRecording.getId()) { + mScheduledRecording = recording; + updateActions(); + return; + } + } + } + + @Override + public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) { + onScheduledRecordingStatusChanged(scheduledRecordings); + } + + private static class MyImageLoaderCallback + extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> { + private final Context mContext; + private final int mLoadType; + + public MyImageLoaderCallback( + ProgramDetailsFragment fragment, int loadType, Context context) { + super(fragment); + mLoadType = loadType; + mContext = context; + } + + @Override + public void onBitmapLoaded(ProgramDetailsFragment fragment, @Nullable Bitmap bitmap) { + Drawable drawable; + int loadType = mLoadType; + if (bitmap == null) { + Resources res = mContext.getResources(); + drawable = res.getDrawable(R.drawable.dvr_default_poster, null); + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { + loadType &= ~LOAD_BACKGROUND_IMAGE; + fragment.mBackgroundHelper.setBackgroundColor( + res.getColor(R.color.dvr_detail_default_background)); + fragment.mBackgroundHelper.setScrim( + res.getColor(R.color.dvr_detail_default_background_scrim)); + } + } else { + drawable = new BitmapDrawable(mContext.getResources(), bitmap); + } + if (!fragment.isDetached()) { + if ((loadType & LOAD_LOGO_IMAGE) != 0) { + fragment.mDetailsOverview.setImageDrawable(drawable); + } + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { + fragment.mBackgroundHelper.setBackground(drawable); + } + } + } + } +} diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index bb98d974..5ac6bd83 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -20,11 +20,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.app.Activity; -import android.app.AlertDialog; -import android.app.ApplicationErrorReport; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; @@ -38,7 +34,6 @@ 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.TvView.TvInputCallback; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; @@ -47,6 +42,7 @@ import android.os.Bundle; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.AttributeSet; @@ -55,16 +51,17 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.TvViewSession; import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; -import com.android.tv.common.BuildConfig; import com.android.tv.common.CommonConstants; +import com.android.tv.common.compat.TvInputConstantCompat; +import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.util.CommonUtils; import com.android.tv.common.util.Debug; @@ -75,9 +72,11 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; import com.android.tv.data.api.Channel; +import com.android.tv.features.TvFeatures; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.recommendation.NotificationService; +import com.android.tv.ui.api.TunableTvViewPlayingApi; import com.android.tv.util.NetworkUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -95,8 +94,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2; public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3; public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100; - - private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener; + private final AccessibilityManager mAccessibilityManager; @Retention(RetentionPolicy.SOURCE) @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) @@ -132,7 +130,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV private AppLayerTvView mTvView; private TvViewSession mTvViewSession; - private Channel mCurrentChannel; + @Nullable private Channel mCurrentChannel; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; private ParentalControlSettings mParentalControlSettings; @@ -190,8 +188,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV private final ConnectivityManager mConnectivityManager; private final InputSessionManager mInputSessionManager; - private final TvInputCallback mCallback = - new TvInputCallback() { + private int mChannelSignalStrength; + + private final TvInputCallbackCompat mCallback = + new TvInputCallbackCompat() { @Override public void onConnectionFailed(String inputId) { Log.w(TAG, "Failed to bind an input"); @@ -252,7 +252,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } } if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); } } @@ -305,7 +305,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } } if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + // should not change audio track automatically when an audio track or a + // subtitle track is selected + mOnTuneListener.onStreamInfoChanged( + TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO); } } @@ -316,60 +319,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV .log( "Start up of Live TV ends," + " TunableTvView.onVideoAvailable resets timer"); - long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); + Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); Debug.removeTimer(Debug.TAG_START_UP_TIMER); - if (BuildConfig.ENG - && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) { - showAlertDialogForLongStartUp(); - } mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE; updateBlockScreenAndMuting(); if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); } } - private void showAlertDialogForLongStartUp() { - new AlertDialog.Builder(getContext()) - .setTitle(getContext().getString(R.string.settings_send_feedback)) - .setMessage( - "Because the start up time of Live channels is too long," - + " please send feedback") - .setPositiveButton( - android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick( - DialogInterface dialogInterface, int i) { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = - new ApplicationErrorReport(); - report.packageName = - report.processName = - getContext() - .getApplicationContext() - .getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_CRASH; - - // Add the crash info to add title of feedback - // automatically. - ApplicationErrorReport.CrashInfo crash = - new ApplicationErrorReport.CrashInfo(); - crash.exceptionClassName = - "Live TV start up takes long time"; - crash.exceptionMessage = - "The start up time of Live TV is too long"; - report.crashInfo = crash; - - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - getContext().startActivity(intent); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - @Override public void onVideoUnavailable(String inputId, int reason) { if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING @@ -390,12 +348,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } updateBlockScreenAndMuting(); if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true); } switch (reason) { case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: + case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED: mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); break; default: @@ -441,6 +400,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; setTimeShiftAvailable(available); } + + @Override + public void onSignalStrength(String inputId, int value) { + mChannelSignalStrength = value; + if (mOnTuneListener != null) { + mOnTuneListener.onChannelSignalStrength(); + } + } }; public TunableTvView(Context context) { @@ -502,35 +469,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } } }); - View placeholder = findViewById(R.id.placeholder); - placeholder.requestFocus(); - findViewById(R.id.channel_up) - .setOnFocusChangeListener( - (v, hasFocus) -> { - if (hasFocus) { - placeholder.requestFocus(); - if (mOnTalkBackDpadKeyListener != null) { - mOnTalkBackDpadKeyListener.onTalkBackDpadKey( - KeyEvent.KEYCODE_DPAD_UP); - } - } - }); - findViewById(R.id.channel_down) - .setOnFocusChangeListener( - (v, hasFocus) -> { - if (hasFocus) { - placeholder.requestFocus(); - if (mOnTalkBackDpadKeyListener != null) { - mOnTalkBackDpadKeyListener.onTalkBackDpadKey( - KeyEvent.KEYCODE_DPAD_DOWN); - } - } - }); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); } public void initialize( ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) { - mTvView = (AppLayerTvView) findViewById(R.id.tv_view); + mTvView = findViewById(R.id.tv_view); mProgramDataManager = programDataManager; mInputManagerHelper = tvInputManagerHelper; mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); @@ -621,6 +565,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV mIsUnderShrunken = isUnderShrunken; } + public int getChannelSignalStrength() { + return mChannelSignalStrength; + } + + public void resetChannelSignalStrength() { + mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; + } + @Override public boolean isPlaying() { return mStarted; @@ -714,12 +666,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } updateBlockScreenAndMuting(); if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(this); + mOnTuneListener.onStreamInfoChanged(this, true); } return true; } @Override + @Nullable public Channel getCurrentChannel() { return mCurrentChannel; } @@ -795,13 +748,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV void onUnexpectedStop(Channel channel); - void onStreamInfoChanged(StreamInfo info); + void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack); void onChannelRetuned(Uri channel); void onContentBlocked(); void onContentAllowed(); + + void onChannelSignalStrength(); } public void unblockContent(TvContentRating rating) { @@ -869,14 +824,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV mTvView.setOnUnhandledInputEventListener(listener); } - public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) { - mOnTalkBackDpadKeyListener = listener; - } - public void setClosedCaptionEnabled(boolean enabled) { mTvView.setCaptionEnabled(enabled); } + @VisibleForTesting + public void setOnTuneListener(OnTuneListener listener) { + mOnTuneListener = listener; + } + public List<TvTrackInfo> getTracks(int type) { return mTvView.getTracks(type); } @@ -1044,6 +1000,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV if (text != null) { mBlockScreenView.setInfoText(text); } + mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled); } /** @@ -1053,6 +1010,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV private String getBlockScreenText() { // TODO: add a test for this method Resources res = getResources(); + boolean isA11y = mAccessibilityManager.isEnabled(); + if (mScreenBlocked && mParentControlEnabled) { switch (mBlockScreenType) { case BLOCK_SCREEN_TYPE_NO_UI: @@ -1060,7 +1019,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV return ""; case BLOCK_SCREEN_TYPE_NORMAL: if (mCanModifyParentalControls) { - return res.getString(R.string.tvview_channel_locked); + return res.getString( + isA11y + ? R.string.tvview_channel_locked_talkback + : R.string.tvview_channel_locked); } else { return res.getString(R.string.tvview_channel_locked_no_permission); } @@ -1081,15 +1043,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV case BLOCK_SCREEN_TYPE_NORMAL: if (TextUtils.isEmpty(name)) { if (mCanModifyParentalControls) { - return res.getString(R.string.tvview_content_locked); + return res.getString( + isA11y + ? R.string.tvview_content_locked_talkback + : R.string.tvview_content_locked); } else { return res.getString(R.string.tvview_content_locked_no_permission); } } else { if (mCanModifyParentalControls) { return name.equals(res.getString(R.string.unrated_rating_name)) - ? res.getString(R.string.tvview_content_locked_unrated) - : res.getString(R.string.tvview_content_locked_format, name); + ? res.getString( + isA11y + ? R.string + .tvview_content_locked_unrated_talkback + : R.string.tvview_content_locked_unrated) + : res.getString( + isA11y + ? R.string.tvview_content_locked_format_talkback + : R.string.tvview_content_locked_format, + name); } else { return name.equals(res.getString(R.string.unrated_rating_name)) ? res.getString( @@ -1106,6 +1079,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV return res.getString(R.string.tvview_msg_audio_only); case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: return res.getString(R.string.tvview_msg_weak_signal); + case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED: + return res.getString(R.string.msg_channel_unavailable_not_connected); case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE: return getTuneConflictMessage(); default: @@ -1122,7 +1097,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV && (mScreenBlocked || mBlockedContentRating != null || mVideoUnavailableReason - == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)) { + == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN + || mVideoUnavailableReason + == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) { ((Activity) getContext()).finish(); return true; } @@ -1237,20 +1214,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV .setDuration(durationMillis) .setInterpolator(interpolator) .withStartAction( - new Runnable() { - @Override - public void run() { - mFadeState = FADING_OUT; - mActionAfterFade = actionAfterFade; - } + () -> { + mFadeState = FADING_OUT; + mActionAfterFade = actionAfterFade; }) - .withEndAction( - new Runnable() { - @Override - public void run() { - mFadeState = FADED_OUT; - } - }); + .withEndAction(() -> mFadeState = FADED_OUT); } /** Fade in this TunableTvView. Fade in by decreasing the dimming. */ @@ -1264,20 +1232,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV .setDuration(durationMillis) .setInterpolator(interpolator) .withStartAction( - new Runnable() { - @Override - public void run() { - mFadeState = FADING_IN; - mActionAfterFade = actionAfterFade; - } + () -> { + mFadeState = FADING_IN; + mActionAfterFade = actionAfterFade; }) .withEndAction( - new Runnable() { - @Override - public void run() { - mFadeState = FADED_IN; - mDimScreenView.setVisibility(View.GONE); - } + () -> { + mFadeState = FADED_IN; + mDimScreenView.setVisibility(View.GONE); }); } @@ -1298,6 +1260,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV mTimeShiftListener = listener; } + public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) { + mBlockScreenView.setInfoTextOnClickListener(onClickListener); + } + private void setTimeShiftAvailable(boolean isTimeShiftAvailable) { if (mTimeShiftAvailable == isTimeShiftAvailable) { return; @@ -1336,7 +1302,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV /** Plays the media, if the current input supports time-shifting. */ @Override - public void timeshiftPlay() { + public void timeShiftPlay() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } @@ -1348,7 +1314,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV /** Pauses the media, if the current input supports time-shifting. */ @Override - public void timeshiftPause() { + public void timeShiftPause() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } @@ -1364,7 +1330,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ @Override - public void timeshiftRewind(int speed) { + public void timeShiftRewind(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } else { @@ -1384,7 +1350,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ @Override - public void timeshiftFastForward(int speed) { + public void timeShiftFastForward(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } else { @@ -1404,7 +1370,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV * @param timeMs The time in milliseconds to seek to. */ @Override - public void timeshiftSeekTo(long timeMs) { + public void timeShiftSeekTo(long timeMs) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } @@ -1413,14 +1379,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV /** Returns the current playback position in milliseconds. */ @Override - public long timeshiftGetCurrentPositionMs() { + public long timeShiftGetCurrentPositionMs() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } if (DEBUG) { Log.d( TAG, - "timeshiftGetCurrentPositionMs: current position =" + "timeShiftGetCurrentPositionMs: current position =" + Utils.toTimeString(mTimeShiftCurrentPositionMs)); } return mTimeShiftCurrentPositionMs; @@ -1446,12 +1412,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV }; } - /** Listens for dpad actions that are otherwise trapped by talkback */ - public interface OnTalkBackDpadKeyListener { - - void onTalkBackDpadKey(int keycode); - } - /** A listener which receives the notification when the screen is blocked/unblocked. */ public abstract static class OnScreenBlockingChangedListener { /** Called when the screen is blocked/unblocked. */ diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index 222fcb3a..b2854a1f 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -86,19 +86,18 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = { - FLAG_HIDE_OVERLAYS_DEFAULT, - FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, - FLAG_HIDE_OVERLAYS_KEEP_SCENE, - FLAG_HIDE_OVERLAYS_KEEP_DIALOG, - FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, - FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, - FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, - FLAG_HIDE_OVERLAYS_KEEP_MENU, - FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT - } - ) + flag = true, + value = { + FLAG_HIDE_OVERLAYS_DEFAULT, + FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, + FLAG_HIDE_OVERLAYS_KEEP_SCENE, + FLAG_HIDE_OVERLAYS_KEEP_DIALOG, + FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, + FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, + FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, + FLAG_HIDE_OVERLAYS_KEEP_MENU, + FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT + }) private @interface HideOverlayFlag {} // FLAG_HIDE_OVERLAYs must be bitwise exclusive. public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000; @@ -115,20 +114,19 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { @Retention(RetentionPolicy.SOURCE) @IntDef( - flag = true, - value = { - OVERLAY_TYPE_NONE, - OVERLAY_TYPE_MENU, - OVERLAY_TYPE_SIDE_FRAGMENT, - OVERLAY_TYPE_DIALOG, - OVERLAY_TYPE_GUIDE, - OVERLAY_TYPE_SCENE_CHANNEL_BANNER, - OVERLAY_TYPE_SCENE_INPUT_BANNER, - OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH, - OVERLAY_TYPE_SCENE_SELECT_INPUT, - OVERLAY_TYPE_FRAGMENT - } - ) + flag = true, + value = { + OVERLAY_TYPE_NONE, + OVERLAY_TYPE_MENU, + OVERLAY_TYPE_SIDE_FRAGMENT, + OVERLAY_TYPE_DIALOG, + OVERLAY_TYPE_GUIDE, + OVERLAY_TYPE_SCENE_CHANNEL_BANNER, + OVERLAY_TYPE_SCENE_INPUT_BANNER, + OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH, + OVERLAY_TYPE_SCENE_SELECT_INPUT, + OVERLAY_TYPE_FRAGMENT + }) private @interface TvOverlayType {} // OVERLAY_TYPEs must be bitwise exclusive. /** The overlay type which indicates that there are no overlays. */ @@ -176,6 +174,8 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; /** Updates channel banner because of stream info updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6; + /** Updates channel banner because of channel signal updating. */ + public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH = 7; private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources"; private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources"; @@ -287,35 +287,17 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { mSideFragmentManager = new SideFragmentManager( mainActivity, - new Runnable() { - @Override - public void run() { - onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); - hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); - } + () -> { + onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); + hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); }, - new Runnable() { - @Override - public void run() { - showChannelBannerIfHiddenBySideFragment(); - onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); - } + () -> { + showChannelBannerIfHiddenBySideFragment(); + onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); }); // Program Guide - Runnable preShowRunnable = - new Runnable() { - @Override - public void run() { - onOverlayOpened(OVERLAY_TYPE_GUIDE); - } - }; - Runnable postHideRunnable = - new Runnable() { - @Override - public void run() { - onOverlayClosed(OVERLAY_TYPE_GUIDE); - } - }; + Runnable preShowRunnable = () -> onOverlayOpened(OVERLAY_TYPE_GUIDE); + Runnable postHideRunnable = () -> onOverlayClosed(OVERLAY_TYPE_GUIDE); DvrDataManager dvrDataManager = CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null; mProgramGuide = @@ -520,16 +502,13 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); onOverlayOpened(OVERLAY_TYPE_FRAGMENT); runAfterSideFragmentsAreClosed( - new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")"); - mMainActivity - .getFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, fragment, tag) - .commit(); - } + () -> { + if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")"); + mMainActivity + .getFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, fragment, tag) + .commit(); }); } @@ -678,12 +657,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { /** Shows the program guide. */ public void showProgramGuide() { mProgramGuide.show( - new Runnable() { - @Override - public void run() { - hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); - } - }); + () -> hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE)); } /** @@ -855,6 +829,10 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { && lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { mChannelBannerView.updateViews(false); } + } else if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mMainActivity) + && reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH) { + mChannelBannerView.updateChannelSignalStrengthView( + mTvView.getChannelSignalStrength()); } else { mChannelBannerView.updateViews( reason == UPDATE_CHANNEL_BANNER_REASON_TUNE diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java index 5af3e6f2..f60337f1 100644 --- a/src/com/android/tv/ui/TvTransitionManager.java +++ b/src/com/android/tv/ui/TvTransitionManager.java @@ -174,28 +174,19 @@ public class TvTransitionManager extends TransitionManager { mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView); mEmptyScene.setEnterAction( - new Runnable() { - @Override - public void run() { - FrameLayout.LayoutParams emptySceneLayoutParams = - (FrameLayout.LayoutParams) mEmptyView.getLayoutParams(); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams(); - emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop(); - emptySceneLayoutParams.setMarginStart(lp.getMarginStart()); - emptySceneLayoutParams.height = mCurrentSceneView.getHeight(); - emptySceneLayoutParams.width = mCurrentSceneView.getWidth(); - mEmptyView.setLayoutParams(emptySceneLayoutParams); - setCurrentScene(mEmptyScene, mEmptyView); - } - }); - mEmptyScene.setExitAction( - new Runnable() { - @Override - public void run() { - removeAllViewsFromOverlay(); - } + () -> { + FrameLayout.LayoutParams emptySceneLayoutParams = + (FrameLayout.LayoutParams) mEmptyView.getLayoutParams(); + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams(); + emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop(); + emptySceneLayoutParams.setMarginStart(lp.getMarginStart()); + emptySceneLayoutParams.height = mCurrentSceneView.getHeight(); + emptySceneLayoutParams.width = mCurrentSceneView.getWidth(); + mEmptyView.setLayoutParams(emptySceneLayoutParams); + setCurrentScene(mEmptyScene, mEmptyView); }); + mEmptyScene.setExitAction(this::removeAllViewsFromOverlay); mChannelBannerScene = buildScene(mSceneContainer, mChannelBannerView); mInputBannerScene = buildScene(mSceneContainer, mInputBannerView); @@ -274,21 +265,15 @@ public class TvTransitionManager extends TransitionManager { private Scene buildScene(ViewGroup sceneRoot, final TransitionLayout layout) { final Scene scene = new Scene(sceneRoot, (View) layout); scene.setEnterAction( - new Runnable() { - @Override - public void run() { - boolean wasEmptyScene = (mCurrentScene == mEmptyScene); - setCurrentScene(scene, (ViewGroup) layout); - layout.onEnterAction(wasEmptyScene); - } + () -> { + boolean wasEmptyScene = (mCurrentScene == mEmptyScene); + setCurrentScene(scene, (ViewGroup) layout); + layout.onEnterAction(wasEmptyScene); }); scene.setExitAction( - new Runnable() { - @Override - public void run() { - removeAllViewsFromOverlay(); - layout.onExitAction(); - } + () -> { + removeAllViewsFromOverlay(); + layout.onExitAction(); }); return scene; } diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index 7e354db3..b7e8b433 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -43,9 +43,9 @@ import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvOptionsManager; import com.android.tv.data.DisplayMode; +import com.android.tv.features.TvFeatures; import com.android.tv.util.TvSettings; /** @@ -460,12 +460,7 @@ public class TvViewUiManager { return; } mHandler.post( - new Runnable() { - @Override - public void run() { - setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false); - } - }); + () -> setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false)); } }); mTvViewAnimator.addUpdateListener( @@ -496,13 +491,7 @@ public class TvViewUiManager { new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mHandler.post( - new Runnable() { - @Override - public void run() { - mContentView.setBackgroundColor(mBackgroundColor); - } - }); + mHandler.post(() -> mContentView.setBackgroundColor(mBackgroundColor)); } }); } diff --git a/src/com/android/tv/ui/TunableTvViewPlayingApi.java b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java index 3f19b61f..eb1f030d 100644 --- a/src/com/android/tv/ui/TunableTvViewPlayingApi.java +++ b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.ui; +package com.android.tv.ui.api; /** API to play pause and set the volume of a TunableTvView */ public interface TunableTvViewPlayingApi { @@ -27,17 +27,17 @@ public interface TunableTvViewPlayingApi { boolean isTimeShiftAvailable(); - void timeshiftPlay(); + void timeShiftPlay(); - void timeshiftPause(); + void timeShiftPause(); - void timeshiftRewind(int speed); + void timeShiftRewind(int speed); - void timeshiftFastForward(int speed); + void timeShiftFastForward(int speed); - void timeshiftSeekTo(long timeMs); + void timeShiftSeekTo(long timeMs); - long timeshiftGetCurrentPositionMs(); + long timeShiftGetCurrentPositionMs(); /** Used to receive the time-shift events. */ abstract class TimeShiftListener { diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java index 75859792..8bf70de1 100644 --- a/src/com/android/tv/ui/hideable/AutoHideScheduler.java +++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2018 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.hideable; import android.content.Context; diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java index 48b80723..62130b64 100644 --- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java +++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java @@ -37,7 +37,6 @@ import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -213,17 +212,14 @@ public class CustomizeChannelListFragment extends SideFragment { ArrayList<Channel> channels = new ArrayList<>(mChannels); Collections.sort( channels, - new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - boolean lhsHd = isHdChannel(lhs); - boolean rhsHd = isHdChannel(rhs); - if (lhsHd == rhsHd) { - return ChannelNumber.compare( - lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } else { - return lhsHd ? -1 : 1; - } + (Channel lhs, Channel rhs) -> { + boolean lhsHd = isHdChannel(lhs); + boolean rhsHd = isHdChannel(rhs); + if (lhsHd == rhsHd) { + return ChannelNumber.compare( + lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } else { + return lhsHd ? -1 : 1; } }); diff --git a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java index 03b71c8c..7a65247f 100644 --- a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java +++ b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java @@ -20,7 +20,7 @@ import android.media.tv.TvTrackInfo; import android.text.TextUtils; import android.view.KeyEvent; import com.android.tv.R; -import com.android.tv.util.Utils; +import com.android.tv.util.TvTrackInfoUtils; import java.util.ArrayList; import java.util.List; @@ -51,12 +51,13 @@ public class MultiAudioFragment extends SideFragment { List<Item> items = new ArrayList<>(); if (tracks != null) { - boolean needToShowSampleRate = Utils.needToShowSampleRate(getActivity(), tracks); + boolean needToShowSampleRate = TvTrackInfoUtils + .needToShowSampleRate(getActivity(), tracks); int pos = 0; for (final TvTrackInfo track : tracks) { RadioButtonItem item = new MultiAudioOptionItem( - Utils.getMultiAudioString( + TvTrackInfoUtils.getMultiAudioString( getActivity(), track, needToShowSampleRate), track.getId()); if (track.getId().equals(mSelectedTrackId)) { diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index 31d00fa6..aa71fb75 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -16,8 +16,6 @@ package com.android.tv.ui.sidepanel; -import static com.android.tv.TvFeatures.TUNER; - import android.app.ApplicationErrorReport; import android.content.Intent; import android.media.tv.TvInputInfo; @@ -81,10 +79,9 @@ public class SettingsFragment extends SideFragment { customizeChannelListItem.setEnabled(false); items.add(customizeChannelListItem); final MainActivity activity = getMainActivity(); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); boolean hasNewInput = - TvSingletons.getSingletons(getContext()) - .getSetupUtils() - .hasNewInput(activity.getTvInputManagerHelper()); + singletons.getSetupUtils().hasNewInput(activity.getTvInputManagerHelper()); items.add( new ActionItem( getString(R.string.settings_channel_source_item_setup), @@ -127,11 +124,9 @@ public class SettingsFragment extends SideFragment { // It's TBD. } boolean showTrickplaySetting = false; - if (TUNER.isEnabled(getContext())) { + if (singletons.getBuiltInTunerManager().isPresent()) { for (TvInputInfo inputInfo : - TvSingletons.getSingletons(getContext()) - .getTvInputManagerHelper() - .getTvInputInfos(true, true)) { + singletons.getTvInputManagerHelper().getTvInputInfos(true, true)) { if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) { showTrickplaySetting = true; break; diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index 2902ea7f..590f1300 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -342,12 +342,9 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H } if (view.getBackground() instanceof RippleDrawable) { view.postDelayed( - new Runnable() { - @Override - public void run() { - if (mItem != null) { - mItem.onSelected(); - } + () -> { + if (mItem != null) { + mItem.onSelected(); } }, view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration)); diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java index 4e3cf7fb..b14bf78d 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java @@ -41,7 +41,6 @@ import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; public class ChannelsBlockedFragment extends SideFragment { @@ -132,15 +131,11 @@ public class ChannelsBlockedFragment extends SideFragment { mChannels.addAll(getChannelDataManager().getChannelList()); Collections.sort( mChannels, - new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - if (lhs.isBrowsable() != rhs.isBrowsable()) { - return lhs.isBrowsable() ? -1 : 1; - } - return ChannelNumber.compare( - lhs.getDisplayNumber(), rhs.getDisplayNumber()); + (Channel lhs, Channel rhs) -> { + if (lhs.isBrowsable() != rhs.isBrowsable()) { + return lhs.isBrowsable() ? -1 : 1; } + return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); }); final long currentChannelId = getMainActivity().getCurrentChannelId(); diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java index 128fcd1a..d1ae4423 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java @@ -39,6 +39,7 @@ import com.android.tv.ui.sidepanel.RadioButtonItem; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.util.TvSettings; import com.android.tv.util.TvSettings.ContentRatingLevel; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -167,7 +168,7 @@ public class RatingsFragment extends SideFragment { super.onUpdate(); setChecked( mParentalControlSettings.isRatingBlocked( - new TvContentRating[] {TvContentRating.UNRATED})); + ImmutableList.of(TvContentRating.UNRATED))); } @Override @@ -239,7 +240,7 @@ public class RatingsFragment extends SideFragment { // set checked if UNRATED is blocked, and set unchecked otherwise. mBlockUnratedItem.setChecked( mParentalControlSettings.isRatingBlocked( - new TvContentRating[] {TvContentRating.UNRATED})); + ImmutableList.of(TvContentRating.UNRATED))); } notifyItemsChanged(mRatingLevelItems.size()); } diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 60fa3018..b3523952 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -17,6 +17,7 @@ package com.android.tv.util; import android.content.ContentResolver; +import android.content.Context; import android.database.Cursor; import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; @@ -34,9 +35,12 @@ import com.android.tv.data.ChannelImpl; import com.android.tv.data.Program; import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; +import com.google.common.base.Predicate; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +import javax.inject.Qualifier; /** * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. @@ -50,6 +54,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> private static final String TAG = "AsyncDbTask"; private static final boolean DEBUG = false; + /** Annotation for requesting the {@link Executor} for data base access. */ + @Qualifier + public @interface DbExecutor {} + private final Executor mExecutor; boolean mCalledExecuteOnDbThread; @@ -67,23 +75,23 @@ public abstract class AsyncDbTask<Params, Progress, Result> * @param <Result> the type of result returned by {@link #onQuery(Cursor)} */ public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> { - private final ContentResolver mContentResolver; + private final WeakReference<Context> mContextReference; private final Uri mUri; - private final String[] mProjection; private final String mSelection; private final String[] mSelectionArgs; private final String mOrderBy; + private String[] mProjection; public AsyncQueryTask( - Executor executor, - ContentResolver contentResolver, + @DbExecutor Executor executor, + Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { super(executor); - mContentResolver = contentResolver; + mContextReference = new WeakReference<>(context); mUri = uri; mProjection = projection; mSelection = selection; @@ -110,12 +118,35 @@ public abstract class AsyncDbTask<Params, Progress, Result> // This is guaranteed to never call onPostExecute because the task is canceled. return null; } + Context context = mContextReference.get(); + if (context == null) { + return null; + } + if (Utils.isProgramsUri(mUri) + && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) { + mProjection = + TvProviderUtils.addExtraColumnsToProjection( + mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); + } else if (Utils.isRecordedProgramsUri(mUri)) { + if (TvProviderUtils.checkSeriesIdColumn( + context, TvContract.RecordedPrograms.CONTENT_URI)) { + mProjection = + TvProviderUtils.addExtraColumnsToProjection( + mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); + } + if (TvProviderUtils.checkStateColumn( + context, TvContract.RecordedPrograms.CONTENT_URI)) { + mProjection = + TvProviderUtils.addExtraColumnsToProjection( + mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE); + } + } if (DEBUG) { Log.v(TAG, "Starting query for " + this); } try (Cursor c = - mContentResolver.query( - mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) { + context.getContentResolver() + .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) { if (c != null && !isCancelled()) { Result result = onQuery(c); if (DEBUG) { @@ -164,33 +195,25 @@ public abstract class AsyncDbTask<Params, Progress, Result> public AsyncQueryListTask( Executor executor, - ContentResolver contentResolver, + Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { - this( - executor, - contentResolver, - uri, - projection, - selection, - selectionArgs, - orderBy, - null); + this(executor, context, uri, projection, selection, selectionArgs, orderBy, null); } public AsyncQueryListTask( Executor executor, - ContentResolver contentResolver, + Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy, CursorFilter filter) { - super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); + super(executor, context, uri, projection, selection, selectionArgs, orderBy); mFilter = filter; } @@ -202,7 +225,7 @@ public abstract class AsyncDbTask<Params, Progress, Result> // This is guaranteed to never call onPostExecute because the task is canceled. return null; } - if (mFilter != null && !mFilter.filter(c)) { + if (mFilter != null && !mFilter.apply(c)) { continue; } T t = fromCursor(c); @@ -237,13 +260,13 @@ public abstract class AsyncDbTask<Params, Progress, Result> public AsyncQueryItemTask( Executor executor, - ContentResolver contentResolver, + Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { - super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); + super(executor, context, uri, projection, selection, selectionArgs, orderBy); } @Override @@ -283,10 +306,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> /** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */ public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> { - public AsyncChannelQueryTask(Executor executor, ContentResolver contentResolver) { + public AsyncChannelQueryTask(Executor executor, Context context) { super( executor, - contentResolver, + context, TvContract.Channels.CONTENT_URI, ChannelImpl.PROJECTION, null, @@ -302,20 +325,13 @@ public abstract class AsyncDbTask<Params, Progress, Result> /** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */ public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> { - public AsyncProgramQueryTask(Executor executor, ContentResolver contentResolver) { - super( - executor, - contentResolver, - Programs.CONTENT_URI, - Program.PROJECTION, - null, - null, - null); + public AsyncProgramQueryTask(Executor executor, Context context) { + super(executor, context, Programs.CONTENT_URI, Program.PROJECTION, null, null, null); } public AsyncProgramQueryTask( Executor executor, - ContentResolver contentResolver, + Context context, Uri uri, String selection, String[] selectionArgs, @@ -323,7 +339,7 @@ public abstract class AsyncDbTask<Params, Progress, Result> CursorFilter filter) { super( executor, - contentResolver, + context, uri, Program.PROJECTION, selection, @@ -341,9 +357,8 @@ public abstract class AsyncDbTask<Params, Progress, Result> /** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */ public abstract static class AsyncRecordedProgramQueryTask extends AsyncQueryListTask<RecordedProgram> { - public AsyncRecordedProgramQueryTask( - Executor executor, ContentResolver contentResolver, Uri uri) { - super(executor, contentResolver, uri, RecordedProgram.PROJECTION, null, null, null); + public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) { + super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null); } @Override @@ -370,13 +385,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> protected final long mChannelId; public LoadProgramsForChannelTask( - Executor executor, - ContentResolver contentResolver, - long channelId, - @Nullable Range<Long> period) { + Executor executor, Context context, long channelId, @Nullable Range<Long> period) { super( executor, - contentResolver, + context, period == null ? TvContract.buildProgramsUriForChannel(channelId) : TvContract.buildProgramsUriForChannel( @@ -401,11 +413,10 @@ public abstract class AsyncDbTask<Params, Progress, Result> /** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */ public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> { - public AsyncQueryProgramTask( - Executor executor, ContentResolver contentResolver, long programId) { + public AsyncQueryProgramTask(Executor executor, Context context, long programId) { super( executor, - contentResolver, + context, TvContract.buildProgramUri(programId), Program.PROJECTION, null, @@ -420,5 +431,5 @@ public abstract class AsyncDbTask<Params, Progress, Result> } /** An interface which filters the row. */ - public interface CursorFilter extends Filter<Cursor> {} + public interface CursorFilter extends Predicate<Cursor> {} } diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 764689c2..82e8a94a 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -99,17 +99,14 @@ public final class RecurringRunner { long delay = Math.max(next - now, 0); boolean posted = mHandler.postDelayed( - new Runnable() { - @Override - public void run() { - try { - if (DEBUG) Log.i(TAG, "Starting " + mName); - mRunnable.run(); - } catch (Exception e) { - Log.w(TAG, "Error running " + mName, e); - } - postAt(resetNextRunTime()); + () -> { + try { + if (DEBUG) Log.i(TAG, "Starting " + mName); + mRunnable.run(); + } catch (Exception e) { + Log.w(TAG, "Error running " + mName, e); } + postAt(resetNextRunTime()); }, delay); if (!posted) { diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 0d536320..a9b67fa8 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -28,20 +28,25 @@ import android.media.tv.TvInputManager; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.UiThread; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import com.android.tv.TvSingletons; -import com.android.tv.common.BaseApplication; 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.data.ChannelDataManager; import com.android.tv.data.api.Channel; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; +import com.google.common.base.Optional; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; /** A utility class related to input setup. */ +@Singleton public class SetupUtils { private static final String TAG = "SetupUtils"; private static final boolean DEBUG = false; @@ -61,10 +66,12 @@ public class SetupUtils { private final Set<String> mSetUpInputs; private final Set<String> mRecognizedInputs; private boolean mIsFirstTune; - private final String mTunerInputId; + private final Optional<String> mOptionalTunerInputId; - @VisibleForTesting - protected SetupUtils(Context context) { + @Inject + public SetupUtils( + @ApplicationContext Context context, + Optional<BuiltInTunerManager> optionalBuiltInTunerManager) { mContext = context; mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); mSetUpInputs = new ArraySet<>(); @@ -77,16 +84,8 @@ public class SetupUtils { mRecognizedInputs.addAll( mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); - mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); - } - - /** - * Creates an instance of {@link SetupUtils}. - * - * <p><b>WARNING</b> this should only be called by the top level application. - */ - public static SetupUtils createForTvSingletons(Context context) { - return new SetupUtils(context.getApplicationContext()); + mOptionalTunerInputId = + optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId); } /** Additional work after the setup of TV input. */ @@ -124,32 +123,29 @@ public class SetupUtils { TvSingletons tvSingletons = TvSingletons.getSingletons(context); final ChannelDataManager manager = tvSingletons.getChannelDataManager(); manager.updateChannels( - new Runnable() { - @Override - public void run() { - Channel firstChannelForInput = null; - boolean browsableChanged = false; - for (Channel channel : manager.getChannelList()) { - if (channel.getInputId().equals(inputId)) { - if (!channel.isBrowsable()) { - manager.updateBrowsable(channel.getId(), true, true); - browsableChanged = true; - } - if (firstChannelForInput == null) { - firstChannelForInput = channel; - } + () -> { + Channel firstChannelForInput = null; + boolean browsableChanged = false; + for (Channel channel : manager.getChannelList()) { + if (channel.getInputId().equals(inputId)) { + if (!channel.isBrowsable()) { + manager.updateBrowsable(channel.getId(), true, true); + browsableChanged = true; + } + if (firstChannelForInput == null) { + firstChannelForInput = channel; } } - if (firstChannelForInput != null) { - Utils.setLastWatchedChannel(context, firstChannelForInput); - } - if (browsableChanged) { - manager.notifyChannelBrowsableChanged(); - manager.applyUpdatedValuesToDb(); - } - if (postRunnable != null) { - postRunnable.run(); - } + } + if (firstChannelForInput != null) { + Utils.setLastWatchedChannel(context, firstChannelForInput); + } + if (browsableChanged) { + manager.notifyChannelBrowsableChanged(); + manager.applyUpdatedValuesToDb(); + } + if (postRunnable != null) { + postRunnable.run(); } }); } @@ -332,7 +328,9 @@ public class SetupUtils { // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input // from the known inputs so that the input won't appear as a new input whenever the user // plugs in the USB tuner device again. - removedInputList.remove(mTunerInputId); + if (mOptionalTunerInputId.isPresent()) { + removedInputList.remove(mOptionalTunerInputId.get()); + } if (!removedInputList.isEmpty()) { boolean inputPackageDeleted = false; diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java index c4b803b6..fa557ba2 100644 --- a/src/com/android/tv/util/SqlParams.java +++ b/src/com/android/tv/util/SqlParams.java @@ -17,15 +17,16 @@ package com.android.tv.util; import android.database.DatabaseUtils; +import android.support.annotation.Nullable; import java.util.Arrays; /** Convenience class for SQL operations. */ public class SqlParams { private String mTables; - private String mSelection; - private String[] mSelectionArgs; + private @Nullable String mSelection; + private @Nullable String[] mSelectionArgs; - public SqlParams(String tables, String selection, String... selectionArgs) { + public SqlParams(String tables, @Nullable String selection, @Nullable String... selectionArgs) { setTables(tables); setWhere(selection, selectionArgs); } @@ -34,11 +35,11 @@ public class SqlParams { return mTables; } - public String getSelection() { + public @Nullable String getSelection() { return mSelection; } - public String[] getSelectionArgs() { + public @Nullable String[] getSelectionArgs() { return mSelectionArgs; } diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 625fb7b2..cb7d9854 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -19,21 +19,29 @@ package com.android.tv.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.database.ContentObserver; import android.graphics.drawable.Drawable; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvContentRatingSystemInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; +import android.net.Uri; import android.os.Handler; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; -import com.android.tv.TvFeatures; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.compat.TvInputInfoCompat; +import com.android.tv.common.dagger.annotations.ApplicationContext; import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.SystemProperties; +import com.android.tv.features.TvFeatures; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.util.images.ImageCache; @@ -46,7 +54,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; +/** Helper class for {@link TvInputManager}. */ +@UiThread +@Singleton public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; @@ -117,6 +130,12 @@ public class TvInputManagerHelper { }; private static final String META_LABEL_SORT_KEY = "input_sort_key"; + private static final String TV_INPUT_ALLOW_3RD_PARTY_INPUTS = "tv_input_allow_3rd_party_inputs"; + + private static final String[] SYSTEM_INPUT_ID_BLACKLIST = { + "com.google.android.videos/" // Play Movies + }; + /** The default tv input priority to show. */ private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>(); @@ -149,21 +168,24 @@ public class TvInputManagerHelper { private final PackageManager mPackageManager; protected final TvInputManagerInterface mTvInputManager; private final Map<String, Integer> mInputStateMap = new HashMap<>(); - private final Map<String, TvInputInfo> mInputMap = new HashMap<>(); + private final Map<String, TvInputInfoCompat> mInputMap = new HashMap<>(); private final Map<String, String> mTvInputLabels = new ArrayMap<>(); private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>(); private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>(); private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>(); private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>(); - private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>(); + private final Map<String, Drawable> mTvInputApplicationBanners = new ArrayMap<>(); + + private final ContentObserver mContentObserver; private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state); - if (isInBlackList(inputId)) { + TvInputInfo info = mInputMap.get(inputId).getTvInputInfo(); + if (info == null || isInputBlocked(info)) { return; } mInputStateMap.put(inputId, state); @@ -175,12 +197,12 @@ public class TvInputManagerHelper { @Override public void onInputAdded(String inputId) { if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); - if (isInBlackList(inputId)) { + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (info == null || isInputBlocked(info)) { return; } - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { - mInputMap.put(inputId, info); + mInputMap.put(inputId, new TvInputInfoCompat(mContext, info)); CharSequence label = info.loadLabel(mContext); // in tests the label may be missing just use the input id mTvInputLabels.put(inputId, label != null ? label.toString() : inputId); @@ -205,7 +227,7 @@ public class TvInputManagerHelper { mTvInputCustomLabels.remove(inputId); mTvInputApplicationLabels.remove(inputId); mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); + mTvInputApplicationBanners.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); mContentRatingsManager.update(); @@ -219,11 +241,11 @@ public class TvInputManagerHelper { @Override public void onInputUpdated(String inputId) { if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId); - if (isInBlackList(inputId)) { + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (info == null || isInputBlocked(info)) { return; } - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); - mInputMap.put(inputId, info); + mInputMap.put(inputId, new TvInputInfoCompat(mContext, info)); mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); CharSequence inputCustomLabel = info.loadCustomLabel(mContext); if (inputCustomLabel != null) { @@ -231,7 +253,7 @@ public class TvInputManagerHelper { } mTvInputApplicationLabels.remove(inputId); mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); + mTvInputApplicationBanners.remove(inputId); for (TvInputCallback callback : mCallbacks) { callback.onInputUpdated(inputId); } @@ -242,7 +264,10 @@ public class TvInputManagerHelper { @Override public void onTvInputInfoUpdated(TvInputInfo inputInfo) { if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); - mInputMap.put(inputInfo.getId(), inputInfo); + if (isInputBlocked(inputInfo)) { + return; + } + mInputMap.put(inputInfo.getId(), new TvInputInfoCompat(mContext, inputInfo)); mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); if (inputCustomLabel != null) { @@ -264,8 +289,10 @@ public class TvInputManagerHelper { private final ContentRatingsManager mContentRatingsManager; private final ParentalControlSettings mParentalControlSettings; private final Comparator<TvInputInfo> mTvInputInfoComparator; + private boolean mAllow3rdPartyInputs; - public TvInputManagerHelper(Context context) { + @Inject + public TvInputManagerHelper(@ApplicationContext Context context) { this(context, createTvInputManagerWrapper(context)); } @@ -285,6 +312,22 @@ public class TvInputManagerHelper { mContentRatingsManager = new ContentRatingsManager(context, tvInputManager); mParentalControlSettings = new ParentalControlSettings(context); mTvInputInfoComparator = new InputComparatorInternal(this); + mContentObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + String option = uri.getLastPathSegment(); + if (option == null || !option.equals(TV_INPUT_ALLOW_3RD_PARTY_INPUTS)) { + return; + } + boolean previousSetting = mAllow3rdPartyInputs; + updateAllow3rdPartyInputs(); + if (previousSetting == mAllow3rdPartyInputs) { + return; + } + initInputMaps(); + } + }; } public void start() { @@ -297,30 +340,14 @@ public class TvInputManagerHelper { } if (DEBUG) Log.d(TAG, "start"); mStarted = true; + mContext.getContentResolver() + .registerContentObserver( + Settings.Global.getUriFor(TV_INPUT_ALLOW_3RD_PARTY_INPUTS), + true, + mContentObserver); + updateAllow3rdPartyInputs(); mTvInputManager.registerCallback(mInternalCallback, mHandler); - mInputMap.clear(); - mTvInputLabels.clear(); - mTvInputCustomLabels.clear(); - mTvInputApplicationLabels.clear(); - mTvInputApplicationIcons.clear(); - mTvInputAppliactionBanners.clear(); - mInputStateMap.clear(); - mInputIdToPartnerInputMap.clear(); - for (TvInputInfo input : mTvInputManager.getTvInputList()) { - if (DEBUG) Log.d(TAG, "Input detected " + input); - String inputId = input.getId(); - if (isInBlackList(inputId)) { - continue; - } - mInputMap.put(inputId, input); - int state = mTvInputManager.getInputState(inputId); - mInputStateMap.put(inputId, state); - mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input)); - } - SoftPreconditions.checkState( - mInputStateMap.size() == mInputMap.size(), - TAG, - "mInputStateMap not the same size as mInputMap"); + initInputMaps(); mContentRatingsManager.update(); } @@ -329,6 +356,7 @@ public class TvInputManagerHelper { return; } mTvInputManager.unregisterCallback(mInternalCallback); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); mStarted = false; mInputStateMap.clear(); mInputMap.clear(); @@ -336,8 +364,7 @@ public class TvInputManagerHelper { mTvInputCustomLabels.clear(); mTvInputApplicationLabels.clear(); mTvInputApplicationIcons.clear(); - mTvInputAppliactionBanners.clear(); - ; + mTvInputApplicationBanners.clear(); mInputIdToPartnerInputMap.clear(); } @@ -355,6 +382,9 @@ public class TvInputManagerHelper { continue; } TvInputInfo input = getTvInputInfo(pair.getKey()); + if (input == null || isInputBlocked(input)) { + continue; + } if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) { continue; } @@ -460,12 +490,12 @@ public class TvInputManagerHelper { /** Gets the tv input application's banner. */ public Drawable getTvInputApplicationBanner(String inputId) { - return mTvInputAppliactionBanners.get(inputId); + return mTvInputApplicationBanners.get(inputId); } /** Stores the tv input application's banner. */ public void setTvInputApplicationBanner(String inputId, Drawable banner) { - mTvInputAppliactionBanners.put(inputId, banner); + mTvInputApplicationBanners.put(inputId, banner); } /** Returns if TV input exists with the input id. */ @@ -475,7 +505,14 @@ public class TvInputManagerHelper { return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null; } + @Nullable public TvInputInfo getTvInputInfo(String inputId) { + TvInputInfoCompat inputInfo = getTvInputInfoCompat(inputId); + return inputInfo == null ? null : inputInfo.getTvInputInfo(); + } + + @Nullable + public TvInputInfoCompat getTvInputInfoCompat(String inputId) { SoftPreconditions.checkState( mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started."); if (!mStarted) { @@ -494,7 +531,7 @@ public class TvInputManagerHelper { public int getTunerTvInputSize() { int size = 0; - for (TvInputInfo input : mInputMap.values()) { + for (TvInputInfoCompat input : mInputMap.values()) { if (input.getType() == TvInputInfo.TYPE_TUNER) { ++size; } @@ -601,6 +638,61 @@ public class TvInputManagerHelper { return false; } + private void initInputMaps() { + mInputMap.clear(); + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + mTvInputApplicationIcons.clear(); + mTvInputApplicationBanners.clear(); + mInputStateMap.clear(); + mInputIdToPartnerInputMap.clear(); + for (TvInputInfo input : mTvInputManager.getTvInputList()) { + if (DEBUG) { + Log.d(TAG, "Input detected " + input); + } + String inputId = input.getId(); + if (isInputBlocked(input)) { + continue; + } + mInputMap.put(inputId, new TvInputInfoCompat(mContext, input)); + int state = mTvInputManager.getInputState(inputId); + mInputStateMap.put(inputId, state); + mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input)); + } + SoftPreconditions.checkState( + mInputStateMap.size() == mInputMap.size(), + TAG, + "mInputStateMap not the same size as mInputMap"); + } + + private void updateAllow3rdPartyInputs() { + int setting; + try { + setting = + Settings.Global.getInt( + mContext.getContentResolver(), TV_INPUT_ALLOW_3RD_PARTY_INPUTS); + } catch (SettingNotFoundException e) { + mAllow3rdPartyInputs = SystemProperties.ALLOW_THIRD_PARTY_INPUTS.getValue(); + return; + } + mAllow3rdPartyInputs = setting == 1; + } + + private boolean isInputBlocked(TvInputInfo info) { + if (!mAllow3rdPartyInputs) { + if (!isSystemInput(info)) { + return true; + } + for (String id : SYSTEM_INPUT_ID_BLACKLIST) { + if (info.getId().startsWith(id)) { + return true; + } + } + } + return isInBlackList(info.getId()); + } + /** * Default comparator for TvInputInfo. * diff --git a/src/com/android/tv/util/TvProviderUtils.java b/src/com/android/tv/util/TvProviderUtils.java new file mode 100644 index 00000000..6b5aaecc --- /dev/null +++ b/src/com/android/tv/util/TvProviderUtils.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2018 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.util; + +import static java.lang.Boolean.TRUE; + +import android.content.Context; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.StringDef; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.util.Log; +import com.android.tv.data.BaseProgram; +import com.android.tv.features.PartnerFeatures; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** A utility class related to TvProvider. */ +public final class TvProviderUtils { + private static final String TAG = "TvProviderUtils"; + + public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID; + public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE; + + /** Possible extra columns in TV provider. */ + @Retention(RetentionPolicy.SOURCE) + @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE}) + public @interface TvProviderExtraColumn {} + + private static boolean sProgramHasSeriesIdColumn; + private static boolean sRecordedProgramHasSeriesIdColumn; + private static boolean sRecordedProgramHasStateColumn; + + /** + * Checks whether a table contains a series ID column. + * + * <p>This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link + * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be + * run in worker thread. + * + * @return {@code true} if the corresponding table contains a series ID column; {@code false} + * otherwise. + */ + @WorkerThread + public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) { + boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); + if (!canCreateColumn) { + return false; + } + return (Utils.isRecordedProgramsUri(uri) + && checkRecordedProgramTableSeriesIdColumn(context, uri)) + || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri)); + } + + @WorkerThread + private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) { + if (!sProgramHasSeriesIdColumn) { + if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { + sProgramHasSeriesIdColumn = true; + } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { + sProgramHasSeriesIdColumn = true; + } + } + return sProgramHasSeriesIdColumn; + } + + @WorkerThread + private static synchronized boolean checkRecordedProgramTableSeriesIdColumn( + Context context, Uri uri) { + if (!sRecordedProgramHasSeriesIdColumn) { + if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { + sRecordedProgramHasSeriesIdColumn = true; + } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { + sRecordedProgramHasSeriesIdColumn = true; + } + } + return sRecordedProgramHasSeriesIdColumn; + } + + /** + * Checks whether a table contains a state column. + * + * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may + * access to database, so it should be run in worker thread. + * + * @return {@code true} if the corresponding table contains a state column; {@code false} + * otherwise. + */ + @WorkerThread + public static synchronized boolean checkStateColumn(Context context, Uri uri) { + boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); + if (!canCreateColumn) { + return false; + } + return (Utils.isRecordedProgramsUri(uri) + && checkRecordedProgramTableStateColumn(context, uri)); + } + + @WorkerThread + private static synchronized boolean checkRecordedProgramTableStateColumn( + Context context, Uri uri) { + if (!sRecordedProgramHasStateColumn) { + if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) { + sRecordedProgramHasStateColumn = true; + } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) { + sRecordedProgramHasStateColumn = true; + } + } + return sRecordedProgramHasStateColumn; + } + + public static synchronized boolean getProgramHasSeriesIdColumn() { + return TRUE.equals(sProgramHasSeriesIdColumn); + } + + public static synchronized boolean getRecordedProgramHasSeriesIdColumn() { + return TRUE.equals(sRecordedProgramHasSeriesIdColumn); + } + + public static synchronized boolean getRecordedProgramHasStateColumn() { + return TRUE.equals(sRecordedProgramHasStateColumn); + } + + public static String[] addExtraColumnsToProjection(String[] projection, + @TvProviderExtraColumn String column) { + List<String> projectionList = new ArrayList<>(Arrays.asList(projection)); + if (!projectionList.contains(column)) { + projectionList.add(column); + } + projection = projectionList.toArray(projection); + return projection; + } + + /** + * Gets column names of a table + * + * @param uri the corresponding URI of the table + */ + @VisibleForTesting + static Set<String> getExistingColumns(Context context, Uri uri) { + Bundle result = null; + try { + result = + context.getContentResolver() + .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); + } catch (Exception e) { + Log.e(TAG, "Error trying to get existing columns.", e); + } + if (result != null) { + String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); + if (columns != null) { + return new HashSet<>(Arrays.asList(columns)); + } + } + Log.e(TAG, "Query existing column names from " + uri + " returned null"); + return Collections.emptySet(); + } + + /** + * Add a column to the table + * + * @return {@code true} if the column is added successfully; {@code false} otherwise. + */ + private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) { + Bundle extra = new Bundle(); + extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); + extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); + // If the add operation fails, the following just returns null without crashing. + Bundle allColumns = null; + try { + allColumns = + context.getContentResolver() + .call( + contentUri, + TvContract.METHOD_ADD_COLUMN, + contentUri.toString(), + extra); + } catch (Exception e) { + Log.e(TAG, "Error trying to add column.", e); + } + if (allColumns == null) { + Log.w(TAG, "Adding new column failed. Uri=" + contentUri); + } + return allColumns != null; + } + + private TvProviderUtils() {} +} diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java index 09874502..4ec96c62 100644 --- a/src/com/android/tv/util/TvTrackInfoUtils.java +++ b/src/com/android/tv/util/TvTrackInfoUtils.java @@ -15,13 +15,28 @@ */ package com.android.tv.util; +import android.content.Context; import android.media.tv.TvTrackInfo; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.R; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; /** Static utilities for {@link TvTrackInfo}. */ public class TvTrackInfoUtils { + private static final String TAG = "TvTrackInfoUtils"; + private static final int AUDIO_CHANNEL_NONE = 0; + private static final int AUDIO_CHANNEL_MONO = 1; + private static final int AUDIO_CHANNEL_STEREO = 2; + private static final int AUDIO_CHANNEL_SURROUND_6 = 6; + private static final int AUDIO_CHANNEL_SURROUND_8 = 8; + /** * Compares how closely two {@link android.media.tv.TvTrackInfo}s match {@code language}, {@code * channelCount} and {@code id} in that precedence. @@ -34,40 +49,36 @@ public class TvTrackInfoUtils { */ public static Comparator<TvTrackInfo> createComparator( final String id, final String language, final int channelCount) { - return new Comparator<TvTrackInfo>() { - - @Override - public int compare(TvTrackInfo lhs, TvTrackInfo rhs) { - if (lhs == rhs) { - return 0; - } - if (lhs == null) { - return -1; - } - if (rhs == null) { - return 1; - } - // Assumes {@code null} language matches to any language since it means user hasn't - // selected any track before or selected a track without language information. - boolean lhsLangMatch = - language == null || Utils.isEqualLanguage(lhs.getLanguage(), language); - boolean rhsLangMatch = - language == null || Utils.isEqualLanguage(rhs.getLanguage(), language); - if (lhsLangMatch && rhsLangMatch) { - boolean lhsCountMatch = - lhs.getType() != TvTrackInfo.TYPE_AUDIO - || lhs.getAudioChannelCount() == channelCount; - boolean rhsCountMatch = - rhs.getType() != TvTrackInfo.TYPE_AUDIO - || rhs.getAudioChannelCount() == channelCount; - if (lhsCountMatch && rhsCountMatch) { - return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); - } else { - return Boolean.compare(lhsCountMatch, rhsCountMatch); - } + return (TvTrackInfo lhs, TvTrackInfo rhs) -> { + if (Objects.equals(lhs, rhs)) { + return 0; + } + if (lhs == null) { + return -1; + } + if (rhs == null) { + return 1; + } + // Assumes {@code null} language matches to any language since it means user hasn't + // selected any track before or selected a track without language information. + boolean lhsLangMatch = + language == null || Utils.isEqualLanguage(lhs.getLanguage(), language); + boolean rhsLangMatch = + language == null || Utils.isEqualLanguage(rhs.getLanguage(), language); + if (lhsLangMatch && rhsLangMatch) { + boolean lhsCountMatch = + lhs.getType() != TvTrackInfo.TYPE_AUDIO + || lhs.getAudioChannelCount() == channelCount; + boolean rhsCountMatch = + rhs.getType() != TvTrackInfo.TYPE_AUDIO + || rhs.getAudioChannelCount() == channelCount; + if (lhsCountMatch && rhsCountMatch) { + return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); } else { - return Boolean.compare(lhsLangMatch, rhsLangMatch); + return Boolean.compare(lhsCountMatch, rhsCountMatch); } + } else { + return Boolean.compare(lhsLangMatch, rhsLangMatch); } }; } @@ -96,5 +107,132 @@ public class TvTrackInfoUtils { return best; } + public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) { + Set<String> multiAudioStrings = new HashSet<>(); + for (TvTrackInfo track : tracks) { + String multiAudioString = getMultiAudioString(context, track, false); + if (multiAudioStrings.contains(multiAudioString)) { + return true; + } + multiAudioStrings.add(multiAudioString); + } + return false; + } + + public static String getMultiAudioString( + Context context, TvTrackInfo track, boolean showSampleRate) { + if (track.getType() != TvTrackInfo.TYPE_AUDIO) { + throw new IllegalArgumentException("Not an audio track: " + toString(track)); + } + String language = context.getString(R.string.multi_audio_unknown_language); + if (!TextUtils.isEmpty(track.getLanguage())) { + language = new Locale(track.getLanguage()).getDisplayName(); + } else { + Log.d( + TAG, + "No language information found for the audio track: " + + toString(track) + ); + } + + StringBuilder metadata = new StringBuilder(); + switch (track.getAudioChannelCount()) { + case AUDIO_CHANNEL_NONE: + break; + case AUDIO_CHANNEL_MONO: + metadata.append(context.getString(R.string.multi_audio_channel_mono)); + break; + case AUDIO_CHANNEL_STEREO: + metadata.append(context.getString(R.string.multi_audio_channel_stereo)); + break; + case AUDIO_CHANNEL_SURROUND_6: + metadata.append(context.getString(R.string.multi_audio_channel_surround_6)); + break; + case AUDIO_CHANNEL_SURROUND_8: + metadata.append(context.getString(R.string.multi_audio_channel_surround_8)); + break; + default: + if (track.getAudioChannelCount() > 0) { + metadata.append( + context.getString( + R.string.multi_audio_channel_suffix, + track.getAudioChannelCount())); + } else { + Log.d( + TAG, + "Invalid audio channel count (" + + track.getAudioChannelCount() + + ") found for the audio track: " + + toString(track)); + } + break; + } + if (showSampleRate) { + int sampleRate = track.getAudioSampleRate(); + if (sampleRate > 0) { + if (metadata.length() > 0) { + metadata.append(", "); + } + int integerPart = sampleRate / 1000; + int tenths = (sampleRate % 1000) / 100; + metadata.append(integerPart); + if (tenths != 0) { + metadata.append("."); + metadata.append(tenths); + } + metadata.append("kHz"); + } + } + + if (metadata.length() == 0) { + return language; + } + return context.getString( + R.string.multi_audio_display_string_with_channel, language, metadata.toString()); + } + + private static String trackTypeToString(int trackType) { + switch (trackType) { + case TvTrackInfo.TYPE_AUDIO: + return "Audio"; + case TvTrackInfo.TYPE_VIDEO: + return "Video"; + case TvTrackInfo.TYPE_SUBTITLE: + return "Subtitle"; + default: + return "Invalid Type"; + } + } + + public static String toString(TvTrackInfo info) { + int trackType = info.getType(); + return "TvTrackInfo{" + + "type=" + + trackTypeToString(trackType) + + ", id=" + + info.getId() + + ", language=" + + info.getLanguage() + + ", description=" + + info.getDescription() + + (trackType == TvTrackInfo.TYPE_AUDIO + ? + (", audioChannelCount=" + + info.getAudioChannelCount() + + ", audioSampleRate=" + + info.getAudioSampleRate()) : "") + + (trackType == TvTrackInfo.TYPE_VIDEO + ? + (", videoWidth=" + + info.getVideoWidth() + + ", videoHeight=" + + info.getVideoHeight() + + ", videoFrameRate=" + + info.getVideoFrameRate() + + ", videoPixelAspectRatio=" + + info.getVideoPixelAspectRatio()) : "") + + "}"; + } + private TvTrackInfoUtils() {} } diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index a75bd446..51173739 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -29,7 +29,6 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvContract.Programs.Genres; import android.media.tv.TvInputInfo; -import android.media.tv.TvTrackInfo; import android.net.Uri; import android.os.Looper; import android.preference.PreferenceManager; @@ -42,6 +41,7 @@ import android.util.Log; import android.view.View; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.common.BaseSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.util.Clock; import com.android.tv.data.GenreItems; @@ -99,12 +99,6 @@ public class Utils { private static final int VIDEO_ULTRA_HD_WIDTH = 2048; private static final int VIDEO_ULTRA_HD_HEIGHT = 1536; - private static final int AUDIO_CHANNEL_NONE = 0; - private static final int AUDIO_CHANNEL_MONO = 1; - private static final int AUDIO_CHANNEL_STEREO = 2; - private static final int AUDIO_CHANNEL_SURROUND_6 = 6; - private static final int AUDIO_CHANNEL_SURROUND_8 = 8; - private static final long RECORDING_FAILED_REASON_NONE = 0; private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); @@ -141,6 +135,7 @@ public class Utils { return sb.toString(); } + @Nullable @WorkerThread public static String getInputIdForChannel(Context context, long channelId) { if (channelId == Channel.INVALID_ID) { @@ -153,6 +148,8 @@ public class Utils { if (cursor != null && cursor.moveToNext()) { return Utils.intern(cursor.getString(0)); } + } catch (Exception e) { + Log.e(TAG, "Error get input id for channel", e); } return null; } @@ -325,8 +322,17 @@ public class Utils { Uri uri = TvContract.buildProgramsUriForChannel( TvContract.buildChannelUri(channelId), timeMs, timeMs); - try (Cursor cursor = - context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) { + ContentResolver resolver = context.getContentResolver(); + + String[] projection = Program.PROJECTION; + if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) { + if (Utils.isProgramsUri(uri)) { + projection = + TvProviderUtils.addExtraColumnsToProjection( + projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); + } + } + try (Cursor cursor = resolver.query(uri, projection, null, null, null)) { if (cursor != null && cursor.moveToNext()) { return Program.fromCursor(cursor); } @@ -360,11 +366,10 @@ public class Utils { Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) { return getDurationString( context, - System.currentTimeMillis(), + ((BaseSingletons) context.getApplicationContext()).getClock(), startUtcMillis, endUtcMillis, - useShortFormat, - 0); + useShortFormat); } /** @@ -400,7 +405,7 @@ public class Utils { long startUtcMillis, long endUtcMillis, boolean useShortFormat, - int flag) { + int flags) { return getDurationString( context, startUtcMillis, @@ -408,7 +413,7 @@ public class Utils { useShortFormat, !isInGivenDay(baseMillis, startUtcMillis), true, - flag); + flags); } /** @@ -422,16 +427,20 @@ public class Utils { boolean useShortFormat, boolean showDate, boolean showTime, - int flag) { - flag |= + int flags) { + flags |= DateUtils.FORMAT_ABBREV_MONTH | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); SoftPreconditions.checkArgument(showTime || showDate); if (showTime) { - flag |= DateUtils.FORMAT_SHOW_TIME; + flags |= DateUtils.FORMAT_SHOW_TIME; } if (showDate) { - flag |= DateUtils.FORMAT_SHOW_DATE; + flags |= DateUtils.FORMAT_SHOW_DATE; + } + if (!showDate || (flags & DateUtils.FORMAT_SHOW_YEAR) == 0) { + // year is not shown unless DateUtils.FORMAT_SHOW_YEAR is set explicitly + flags |= DateUtils.FORMAT_NO_YEAR; } if (startUtcMillis != endUtcMillis && useShortFormat) { // Do special handling for 12:00 AM when checking if it's in the given day. @@ -443,15 +452,15 @@ public class Utils { // Subtracting one day is needed because {@link DateUtils@formatDateRange} // automatically shows date if the duration covers multiple days. return DateUtils.formatDateRange( - context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag); + context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flags); } } // Workaround of b/28740989. // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM. - String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag); + String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flags); return startUtcMillis == endUtcMillis || dateRange.contains("–") ? dateRange - : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); + : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flags); } /** @@ -572,86 +581,6 @@ public class Utils { return ""; } - public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) { - Set<String> multiAudioStrings = new HashSet<>(); - for (TvTrackInfo track : tracks) { - String multiAudioString = getMultiAudioString(context, track, false); - if (multiAudioStrings.contains(multiAudioString)) { - return true; - } - multiAudioStrings.add(multiAudioString); - } - return false; - } - - public static String getMultiAudioString( - Context context, TvTrackInfo track, boolean showSampleRate) { - if (track.getType() != TvTrackInfo.TYPE_AUDIO) { - throw new IllegalArgumentException("Not an audio track: " + track); - } - String language = context.getString(R.string.multi_audio_unknown_language); - if (!TextUtils.isEmpty(track.getLanguage())) { - language = new Locale(track.getLanguage()).getDisplayName(); - } else { - Log.d(TAG, "No language information found for the audio track: " + track); - } - - StringBuilder metadata = new StringBuilder(); - switch (track.getAudioChannelCount()) { - case AUDIO_CHANNEL_NONE: - break; - case AUDIO_CHANNEL_MONO: - metadata.append(context.getString(R.string.multi_audio_channel_mono)); - break; - case AUDIO_CHANNEL_STEREO: - metadata.append(context.getString(R.string.multi_audio_channel_stereo)); - break; - case AUDIO_CHANNEL_SURROUND_6: - metadata.append(context.getString(R.string.multi_audio_channel_surround_6)); - break; - case AUDIO_CHANNEL_SURROUND_8: - metadata.append(context.getString(R.string.multi_audio_channel_surround_8)); - break; - default: - if (track.getAudioChannelCount() > 0) { - metadata.append( - context.getString( - R.string.multi_audio_channel_suffix, - track.getAudioChannelCount())); - } else { - Log.d( - TAG, - "Invalid audio channel count (" - + track.getAudioChannelCount() - + ") found for the audio track: " - + track); - } - break; - } - if (showSampleRate) { - int sampleRate = track.getAudioSampleRate(); - if (sampleRate > 0) { - if (metadata.length() > 0) { - metadata.append(", "); - } - int integerPart = sampleRate / 1000; - int tenths = (sampleRate % 1000) / 100; - metadata.append(integerPart); - if (tenths != 0) { - metadata.append("."); - metadata.append(tenths); - } - metadata.append("kHz"); - } - } - - if (metadata.length() == 0) { - return language; - } - return context.getString( - R.string.multi_audio_display_string_with_channel, language, metadata.toString()); - } - public static boolean isEqualLanguage(String lang1, String lang2) { if (lang1 == null) { return lang2 == null; @@ -708,7 +637,6 @@ public class Utils { if (fullFormat) { return new Date(timeMillis).toString(); } else { - long currentTime = System.currentTimeMillis(); return (String) DateUtils.formatSameDayTime( timeMillis, @@ -815,8 +743,11 @@ public class Utils { /** Checks whether the input is internal or not. */ public static boolean isInternalTvInput(Context context, String inputId) { - return context.getPackageName() - .equals(ComponentName.unflattenFromString(inputId).getPackageName()); + ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId); + if (unflattenInputId == null) { + return false; + } + return context.getPackageName().equals(unflattenInputId.getPackageName()); } /** Returns the TV input for the given {@code program}. */ diff --git a/src/com/android/tv/util/images/BitmapUtils.java b/src/com/android/tv/util/images/BitmapUtils.java index d6bd5a31..39524503 100644 --- a/src/com/android/tv/util/images/BitmapUtils.java +++ b/src/com/android/tv/util/images/BitmapUtils.java @@ -20,13 +20,16 @@ import android.content.ContentResolver; import android.content.Context; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.TrafficStats; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.android.tv.common.util.NetworkTrafficTags; @@ -88,6 +91,19 @@ public final class BitmapUtils { calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight)); } + @Nullable + public static Bitmap drawableToBitmap(Drawable drawable) { + if (drawable == null) { + return null; + } + Bitmap bm = Bitmap.createBitmap( + drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(bm); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bm; + } + /** Decode large sized bitmap into requested size. */ public static ScaledBitmapInfo decodeSampledBitmapFromUriString( Context context, String uriString, int reqWidth, int reqHeight) { diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java index e844e2ca..d2ad0eb1 100644 --- a/src/com/android/tv/util/images/ImageLoader.java +++ b/src/com/android/tv/util/images/ImageLoader.java @@ -24,7 +24,6 @@ import android.media.tv.TvInputInfo; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; -import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; @@ -145,22 +144,14 @@ public final class ImageLoader { final Context appContext = context.getApplicationContext(); getMainHandler() .post( - new Runnable() { - @Override - @MainThread - public void run() { - // Calling from the main thread prevents a - // ConcurrentModificationException - // in LoadBitmapTask.onPostExecute + () -> doLoadBitmap( appContext, uriString, maxWidth, maxHeight, null, - AsyncTask.SERIAL_EXECUTOR); - } - }); + AsyncTask.SERIAL_EXECUTOR)); } } @@ -423,14 +414,12 @@ public final class ImageLoader { @Override public ScaledBitmapInfo doGetBitmapInBackground() { Drawable drawable = mInfo.loadIcon(mAppContext); - if (!(drawable instanceof BitmapDrawable)) { - return null; - } - Bitmap original = ((BitmapDrawable) drawable).getBitmap(); - if (original == null) { - return null; - } - return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight); + Bitmap bm = drawable instanceof BitmapDrawable + ? ((BitmapDrawable) drawable).getBitmap() + : BitmapUtils.drawableToBitmap(drawable); + return bm == null + ? null + : BitmapUtils.createScaledBitmapInfo(getKey(), bm, mMaxWidth, mMaxHeight); } /** Returns key of TV input logo. */ diff --git a/tests/common/Android.mk b/tests/common/Android.mk index 3ab16c02..7a111d0c 100644 --- a/tests/common/Android.mk +++ b/tests/common/Android.mk @@ -8,11 +8,11 @@ LOCAL_SRC_FILES := \ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-annotations \ - android-support-test \ - guava \ + androidx.test.runner \ + androidx.test.rules \ + tv-guava-android-jar \ mockito-target \ - platform-robolectric-3.6.2-prebuilt \ - truth-0-36-prebuilt-jar \ + tv-lib-truth \ ub-uiautomator \ # Link tv-common as shared library to avoid the problem of initialization of the constants diff --git a/tests/common/AndroidManifest.xml b/tests/common/AndroidManifest.xml index 8afd8dc9..3a769a8d 100644 --- a/tests/common/AndroidManifest.xml +++ b/tests/common/AndroidManifest.xml @@ -18,6 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.testing" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application /> </manifest> diff --git a/tests/common/src/com/android/tv/testing/DbTestingUtils.java b/tests/common/src/com/android/tv/testing/DbTestingUtils.java index 53e26ca7..e71a714f 100644 --- a/tests/common/src/com/android/tv/testing/DbTestingUtils.java +++ b/tests/common/src/com/android/tv/testing/DbTestingUtils.java @@ -29,7 +29,7 @@ public final class DbTestingUtils { while (cursor.moveToNext()) { List<String> row = new ArrayList<>(colCount); for (int i = 0; i < colCount; i++) { - row.add(cursor.getString(i)); + row.add(cursor.isNull(i) ? "null" : cursor.getString(i)); } result.add(row); } diff --git a/tests/common/src/com/android/tv/testing/EpgTestData.java b/tests/common/src/com/android/tv/testing/EpgTestData.java index 49a92181..362f336a 100644 --- a/tests/common/src/com/android/tv/testing/EpgTestData.java +++ b/tests/common/src/com/android/tv/testing/EpgTestData.java @@ -30,18 +30,19 @@ import java.util.concurrent.TimeUnit; /** EPG data for use in tests. */ public abstract class EpgTestData { - public static final android.support.media.tv.Channel CHANNEL_10 = - new android.support.media.tv.Channel.Builder() + public static final androidx.tvprovider.media.tv.Channel CHANNEL_10 = + new androidx.tvprovider.media.tv.Channel.Builder() .setDisplayName("Channel TEN") .setDisplayNumber("10") + .setNetworkAffiliation("Channel 10 Network Affiliation") .build(); - public static final android.support.media.tv.Channel CHANNEL_11 = - new android.support.media.tv.Channel.Builder() + public static final androidx.tvprovider.media.tv.Channel CHANNEL_11 = + new androidx.tvprovider.media.tv.Channel.Builder() .setDisplayName("Channel Eleven") .setDisplayNumber("11") .build(); - public static final android.support.media.tv.Channel CHANNEL_90_2 = - new android.support.media.tv.Channel.Builder() + public static final androidx.tvprovider.media.tv.Channel CHANNEL_90_2 = + new androidx.tvprovider.media.tv.Channel.Builder() .setDisplayName("Channel Ninety dot Two") .setDisplayNumber("90.2") .build(); @@ -162,21 +163,23 @@ public abstract class EpgTestData { loadData(testSingletonApp.fakeClock, testSingletonApp.epgReader); } - private static Iterable<Channel> toTvChannels(android.support.media.tv.Channel... channels) { + private static Iterable<Channel> toTvChannels( + androidx.tvprovider.media.tv.Channel... channels) { return Iterables.transform( ImmutableList.copyOf(channels), - new Function<android.support.media.tv.Channel, Channel>() { + new Function<androidx.tvprovider.media.tv.Channel, Channel>() { @Override - public Channel apply(android.support.media.tv.Channel original) { + public Channel apply(androidx.tvprovider.media.tv.Channel original) { return toTvChannel(original); } }); } - public static Channel toTvChannel(android.support.media.tv.Channel original) { + public static Channel toTvChannel(androidx.tvprovider.media.tv.Channel original) { return new ChannelImpl.Builder() .setDisplayName(original.getDisplayName()) .setDisplayNumber(original.getDisplayNumber()) + .setNetworkAffiliation(original.getNetworkAffiliation()) // TODO implement the reset .build(); } diff --git a/tests/common/src/com/android/tv/testing/FakeEpgReader.java b/tests/common/src/com/android/tv/testing/FakeEpgReader.java index 710ada55..fb35c652 100644 --- a/tests/common/src/com/android/tv/testing/FakeEpgReader.java +++ b/tests/common/src/com/android/tv/testing/FakeEpgReader.java @@ -37,6 +37,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** Fake {@link EpgReader} for testing. */ @@ -93,7 +94,15 @@ public final class FakeEpgReader implements EpgReader { if (match != null) { ChannelImpl updatedChannel = new ChannelImpl.Builder(match).build(); updatedChannel.setLogoUri(channel.getLogoUri()); - result.add(EpgChannel.createEpgChannel(updatedChannel, channel.getDisplayNumber())); + boolean dbUpdateNeeded = false; + if (!Objects.equals( + channel.getNetworkAffiliation(), updatedChannel.getNetworkAffiliation())) { + dbUpdateNeeded = true; + updatedChannel.setNetworkAffiliation(channel.getNetworkAffiliation()); + } + result.add( + EpgChannel.createEpgChannel( + updatedChannel, channel.getDisplayNumber(), dbUpdateNeeded)); } } return result; diff --git a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java b/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java deleted file mode 100644 index 89e6a0a2..00000000 --- a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 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.testing; - -import android.text.TextUtils; -import com.android.tv.common.config.api.RemoteConfig; -import java.util.HashMap; -import java.util.Map; - -/** Fake {@link RemoteConfig} suitable for testing. */ -public class FakeRemoteConfig implements RemoteConfig { - public final Map<String, String> values = new HashMap(); - - @Override - public void fetch(OnRemoteConfigUpdatedListener listener) {} - - @Override - public String getString(String key) { - return values.get(key); - } - - @Override - public boolean getBoolean(String key) { - String value = values.get(key); - return TextUtils.isEmpty(value) ? false : Boolean.valueOf(key); - } - - @Override - public long getLong(String key) { - return getLong(key, 0); - } - - @Override - public long getLong(String key, long defaultValue) { - if (values.containsKey(key)) { - String value = values.get(key); - return TextUtils.isEmpty(value) ? defaultValue : Long.valueOf(value); - } - return defaultValue; - } -} diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/FakeTvProvider.java index 24c26f39..20903c60 100644 --- a/tests/common/src/com/android/tv/testing/FakeTvProvider.java +++ b/tests/common/src/com/android/tv/testing/FakeTvProvider.java @@ -44,16 +44,16 @@ import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.preference.PreferenceManager; import android.provider.BaseColumns; import android.support.annotation.VisibleForTesting; -import android.support.media.tv.TvContractCompat; -import android.support.media.tv.TvContractCompat.BaseTvColumns; -import android.support.media.tv.TvContractCompat.Channels; -import android.support.media.tv.TvContractCompat.PreviewPrograms; -import android.support.media.tv.TvContractCompat.Programs; -import android.support.media.tv.TvContractCompat.Programs.Genres; -import android.support.media.tv.TvContractCompat.RecordedPrograms; -import android.support.media.tv.TvContractCompat.WatchNextPrograms; import android.text.TextUtils; import android.util.Log; +import androidx.tvprovider.media.tv.TvContractCompat; +import androidx.tvprovider.media.tv.TvContractCompat.BaseTvColumns; +import androidx.tvprovider.media.tv.TvContractCompat.Channels; +import androidx.tvprovider.media.tv.TvContractCompat.PreviewPrograms; +import androidx.tvprovider.media.tv.TvContractCompat.Programs; +import androidx.tvprovider.media.tv.TvContractCompat.Programs.Genres; +import androidx.tvprovider.media.tv.TvContractCompat.RecordedPrograms; +import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms; import com.android.tv.util.SqlParams; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; diff --git a/tests/common/src/com/android/tv/testing/TestSingletonApp.java b/tests/common/src/com/android/tv/testing/TestSingletonApp.java index f55ed8d4..f1a98ff5 100644 --- a/tests/common/src/com/android/tv/testing/TestSingletonApp.java +++ b/tests/common/src/com/android/tv/testing/TestSingletonApp.java @@ -17,9 +17,6 @@ package com.android.tv.testing; import android.app.Application; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; import android.media.tv.TvInputManager; import android.os.AsyncTask; import com.android.tv.InputSessionManager; @@ -28,9 +25,14 @@ import com.android.tv.TvSingletons; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; import com.android.tv.common.BaseApplication; -import com.android.tv.common.config.api.RemoteConfig; import com.android.tv.common.experiments.ExperimentLoader; +import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags; +import com.android.tv.common.flags.impl.DefaultCloudEpgFlags; +import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags; +import com.android.tv.common.flags.impl.DefaultTunerFlags; +import com.android.tv.common.flags.impl.DefaultUiFlags; import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.common.util.Clock; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.PreviewDataManager; @@ -43,21 +45,27 @@ import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.StubPerformanceMonitor; +import com.android.tv.perf.stub.StubPerformanceMonitor; import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl; import com.android.tv.testing.testdata.TestData; -import com.android.tv.tuner.TunerInputController; +import com.android.tv.tuner.singletons.TunerSingletons; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.source.TunerTsStreamerManager; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.account.AccountHelper; +import com.google.common.base.Optional; import java.util.concurrent.Executor; import javax.inject.Provider; /** Test application for Live TV. */ -public class TestSingletonApp extends Application implements TvSingletons { +public class TestSingletonApp extends Application + implements TvSingletons, TunerSingletons, HasSingletons<TvSingletons> { public final FakeClock fakeClock = FakeClock.createWithCurrentTime(); public final FakeEpgReader epgReader = new FakeEpgReader(fakeClock); - public final FakeRemoteConfig remoteConfig = new FakeRemoteConfig(); public final FakeEpgFetcher epgFetcher = new FakeEpgFetcher(); public FakeTvInputManagerHelper tvInputManagerHelper; @@ -66,19 +74,27 @@ public class TestSingletonApp extends Application implements TvSingletons { public DvrDataManager mDvrDataManager; private final Provider<EpgReader> mEpgReaderProvider = SingletonProvider.create(epgReader); - private TunerInputController mTunerInputController; + private final Optional<BuiltInTunerManager> mBuiltInTunerManagerOptional = Optional.absent(); + private final DefaultBackendKnobsFlags mBackendKnobs = new DefaultBackendKnobsFlags(); + private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags(); + private final DefaultUiFlags mUiFlags = new DefaultUiFlags(); + private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags = + new DefaultConcurrentDvrPlaybackFlags(); + private final TsDataSourceManager.Factory mTsDataSourceManagerFactory = + new TsDataSourceManager.Factory(() -> new TunerTsStreamerManager(null)); + private final TunerSessionFactoryImpl mTunerSessionFactory = + new TunerSessionFactoryImpl( + new DefaultTunerFlags(), + mConcurrentDvrPlaybackFlags, + mTsDataSourceManagerFactory); private PerformanceMonitor mPerformanceMonitor; private ChannelDataManager mChannelDataManager; @Override public void onCreate() { super.onCreate(); - mTunerInputController = - new TunerInputController( - ComponentName.unflattenFromString(getEmbeddedTunerInputId())); - tvInputManagerHelper = new FakeTvInputManagerHelper(this); - setupUtils = SetupUtils.createForTvSingletons(this); + setupUtils = new SetupUtils(this, mBuiltInTunerManagerOptional); tvInputManagerHelper.start(); mChannelDataManager = new ChannelDataManager(this, tvInputManagerHelper); mChannelDataManager.start(); @@ -154,7 +170,7 @@ public class TestSingletonApp extends Application implements TvSingletons { @Override public InputSessionManager getInputSessionManager() { - return null; + return new InputSessionManager(this); } @Override @@ -183,8 +199,8 @@ public class TestSingletonApp extends Application implements TvSingletons { } @Override - public TunerInputController getTunerInputController() { - return mTunerInputController; + public Optional<BuiltInTunerManager> getBuiltInTunerManager() { + return mBuiltInTunerManagerOptional; } @Override @@ -213,16 +229,6 @@ public class TestSingletonApp extends Application implements TvSingletons { } @Override - public RemoteConfig getRemoteConfig() { - return remoteConfig; - } - - @Override - public Intent getTunerSetupIntent(Context context) { - return null; - } - - @Override public boolean isRunningInMainProcess() { return false; } @@ -244,4 +250,38 @@ public class TestSingletonApp extends Application implements TvSingletons { public Executor getDbExecutor() { return AsyncTask.SERIAL_EXECUTOR; } + + @Override + public DefaultBackendKnobsFlags getBackendKnobs() { + return mBackendKnobs; + } + + @Override + public DefaultCloudEpgFlags getCloudEpgFlags() { + return mCloudEpgFlags; + } + + @Override + public DefaultUiFlags getUiFlags() { + return mUiFlags; + } + + @Override + public BuildType getBuildType() { + return BuildType.ENG; + } + + @Override + public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() { + return mConcurrentDvrPlaybackFlags; + } + + public TunerSessionFactory getTunerSessionFactory() { + return mTunerSessionFactory; + } + + @Override + public TvSingletons singletons() { + return this; + } } diff --git a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java index 666f8181..495ff20d 100644 --- a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java +++ b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java @@ -15,12 +15,12 @@ */ package com.android.tv.testing.activities; -import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getInstrumentation; import android.content.Context; import android.os.SystemClock; -import android.support.test.rule.ActivityTestRule; import android.text.TextUtils; +import androidx.test.rule.ActivityTestRule; import com.android.tv.MainActivity; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.api.Channel; diff --git a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java index 6d801425..3e7b608e 100644 --- a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java +++ b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java @@ -22,7 +22,7 @@ import android.media.tv.TvContentRating; import android.media.tv.TvContract; import com.android.tv.testing.R; import com.android.tv.testing.utils.Utils; -import java.util.Arrays; +import com.google.common.collect.ImmutableList; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -100,7 +100,7 @@ public final class ProgramInfo { public final String description; public final long durationMs; public final String genre; - public final TvContentRating[] contentRatings; + public final ImmutableList<TvContentRating> contentRatings; public final String resourceUri; public static ProgramInfo fromCursor(Cursor c) { @@ -129,7 +129,7 @@ public final class ProgramInfo { String posterArtUri, String description, long durationMs, - TvContentRating[] contentRatings, + ImmutableList<TvContentRating> contentRatings, String genre, String resourceUri) { this.title = title; @@ -248,7 +248,7 @@ public final class ProgramInfo { && Objects.equals(posterArtUri, that.posterArtUri) && Objects.equals(description, that.description) && Objects.equals(genre, that.genre) - && Arrays.equals(contentRatings, that.contentRatings) + && Objects.equals(contentRatings, that.contentRatings) && Objects.equals(resourceUri, that.resourceUri); } @@ -265,7 +265,7 @@ public final class ProgramInfo { private String mPosterArtUri = GEN_POSTER; private String mDescription; private long mDurationMs = GEN_DURATION; - private TvContentRating[] mContentRatings; + private ImmutableList<TvContentRating> mContentRatings; private String mGenre = GEN_GENRE; private String mResourceUri; @@ -304,7 +304,7 @@ public final class ProgramInfo { return this; } - public Builder setContentRatings(TvContentRating[] contentRatings) { + public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) { mContentRatings = contentRatings; return this; } diff --git a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java index b8a055c7..66707fb6 100644 --- a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java +++ b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java @@ -204,11 +204,7 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { recordedProgram.getId() == RecordedProgram.ID_NOT_SET, TAG, "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram); - recordedProgram = - RecordedProgram - .buildFrom(recordedProgram) - .setId(mNextId.incrementAndGet()) - .build(); + recordedProgram = recordedProgram.withId(mNextId.incrementAndGet()); } mRecordedPrograms.put(recordedProgram.getId(), recordedProgram); notifyRecordedProgramsAdded(recordedProgram); diff --git a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java deleted file mode 100644 index aaaa11df..00000000 --- a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2018 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.testing.robo; - -import android.content.ContentProvider; -import android.content.pm.ProviderInfo; -import org.robolectric.Robolectric; -import org.robolectric.android.controller.ContentProviderController; -import org.robolectric.shadows.ShadowContentResolver; - -/** Static utilities for using content providers in tests. */ -public final class ContentProviders { - - /** Builds creates and register a ContentProvider with the given authority. */ - public static <T extends ContentProvider> T register(Class<T> providerClass, String authority) { - ProviderInfo info = new ProviderInfo(); - info.authority = authority; - ContentProviderController<T> contentProviderController = - Robolectric.buildContentProvider(providerClass); - T provider = contentProviderController.create(info).get(); - provider.onCreate(); - ShadowContentResolver.registerProviderInternal(authority, provider); - return provider; - } - - private ContentProviders() {} -} diff --git a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java deleted file mode 100644 index 9eb79298..00000000 --- a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2018 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.testing.robo; - -import android.media.tv.TvContract; -import com.android.tv.testing.FakeTvProvider; -import com.android.tv.testing.TestSingletonApp; -import com.android.tv.testing.testdata.TestData; -import java.util.concurrent.TimeUnit; -import org.robolectric.Robolectric; - -/** Static utilities for using {@link TestSingletonApp} in roboletric tests. */ -public final class RobotTestAppHelper { - - public static void loadTestData(TestSingletonApp app, TestData testData) { - ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY); - app.loadTestData(testData, TimeUnit.DAYS.toMillis(1)); - Robolectric.flushBackgroundThreadScheduler(); - Robolectric.flushForegroundThreadScheduler(); - } - - private RobotTestAppHelper() {} -} diff --git a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java deleted file mode 100644 index 5a2c41e6..00000000 --- a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2017 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.testing.shadows; - -import android.app.PendingIntent; -import android.content.Context; -import android.media.MediaMetadata; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; - -/** Shadow {@link MediaSession}. */ -@Implements(MediaSession.class) -public class ShadowMediaSession { - - public MediaSession.Callback mCallback; - public PendingIntent mMediaButtonReceiver; - public PendingIntent mSessionActivity; - public PlaybackState mPlaybackState; - public MediaMetadata mMediaMetadata; - public int mFlags; - public boolean mActive; - public boolean mReleased; - - /** Stand-in for the MediaSession constructor with the same parameters. */ - public void __constructor__(Context context, String tag, int userID) { - // This empty method prevents the real MediaSession constructor from being called. - } - - @Implementation - public void setCallback(MediaSession.Callback callback) { - mCallback = callback; - } - - @Implementation - public void setMediaButtonReceiver(PendingIntent mbr) { - mMediaButtonReceiver = mbr; - } - - @Implementation - public void setSessionActivity(PendingIntent activity) { - mSessionActivity = activity; - } - - @Implementation - public void setPlaybackState(PlaybackState state) { - mPlaybackState = state; - } - - @Implementation - public void setMetadata(MediaMetadata metadata) { - mMediaMetadata = metadata; - } - - @Implementation - public void setFlags(int flags) { - mFlags = flags; - } - - @Implementation - public boolean isActive() { - return mActive; - } - - @Implementation - public void setActive(boolean active) { - mActive = active; - } - - @Implementation - public void release() { - mReleased = true; - } -} diff --git a/tests/func/Android.mk b/tests/func/Android.mk index 855e8ebf..53c869ee 100644 --- a/tests/func/Android.mk +++ b/tests/func/Android.mk @@ -10,7 +10,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PACKAGE_NAME := TVFuncTests LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-test \ + androidx.test.runner \ tv-test-common \ ub-uiautomator \ diff --git a/tests/func/AndroidManifest.xml b/tests/func/AndroidManifest.xml index 708dc220..3d7d775f 100644 --- a/tests/func/AndroidManifest.xml +++ b/tests/func/AndroidManifest.xml @@ -18,10 +18,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tests.ui" > - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" /> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" /> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" + android:name="androidx.test.runner.AndroidJUnitRunner" android:label="Live Channel Functional Tests" android:targetPackage="com.android.tv" /> diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java index 600b52b6..c06c859c 100644 --- a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java +++ b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java @@ -16,8 +16,8 @@ package com.android.tv.tests.ui; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; import org.junit.Before; diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java index 53e27f1b..2467de21 100644 --- a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java +++ b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java @@ -15,9 +15,9 @@ */ package com.android.tv.tests.ui; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.uihelper.ByResource; import org.junit.Before; diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java index 1a5ceb45..ac2aad43 100644 --- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java +++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java @@ -16,9 +16,9 @@ package com.android.tv.tests.ui; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.testinput.ChannelStateData; import com.android.tv.testing.testinput.TvTestInputConstants; diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java index 03d30ca4..fa3335d9 100644 --- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java +++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java @@ -23,7 +23,6 @@ import android.content.Context; import android.content.res.Resources; import android.os.Build; import android.os.SystemClock; -import android.support.test.InstrumentationRegistry; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Configurator; import android.support.test.uiautomator.SearchCondition; @@ -31,6 +30,7 @@ import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.Until; import android.view.InputDevice; import android.view.KeyEvent; +import androidx.test.InstrumentationRegistry; import com.android.tv.testing.data.ChannelInfo; import com.android.tv.testing.testinput.ChannelStateData; import com.android.tv.testing.testinput.TestInputControlConnection; diff --git a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java index ee039d7c..bff0e7d7 100644 --- a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java +++ b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java @@ -19,10 +19,10 @@ package com.android.tv.tests.ui; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.uihelper.ByResource; import com.android.tv.testing.uihelper.DialogHelper; diff --git a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java index 7c982782..efc7ecf4 100644 --- a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java +++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java @@ -22,11 +22,11 @@ import static com.android.tv.testing.uihelper.Constants.MENU; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; -import android.support.test.filters.SmallTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; import android.view.KeyEvent; +import androidx.test.filters.SmallTest; import com.android.tv.R; import com.android.tv.testing.testinput.TvTestInputConstants; import com.android.tv.testing.uihelper.Constants; diff --git a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java index 4adf448a..0a6a85d6 100644 --- a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java +++ b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java @@ -15,8 +15,8 @@ */ package com.android.tv.tests.ui; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.guide.ProgramGuide; import com.android.tv.testing.uihelper.Constants; import org.junit.Rule; diff --git a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java index 4b6befe4..73e869f1 100644 --- a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java +++ b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java @@ -15,8 +15,8 @@ */ package com.android.tv.tests.ui; -import android.support.test.filters.LargeTest; import android.support.test.uiautomator.Until; +import androidx.test.filters.LargeTest; import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; import org.junit.Ignore; diff --git a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java index d0ebed91..8998b458 100644 --- a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java +++ b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java @@ -19,11 +19,11 @@ package com.android.tv.tests.ui.dvr; import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitUntilFocused; import android.os.Build; -import android.support.test.filters.MediumTest; -import android.support.test.filters.SdkSuppress; import android.support.test.uiautomator.By; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; +import androidx.test.filters.SdkSuppress; import com.android.tv.R; import com.android.tv.testing.uihelper.ByResource; import com.android.tv.testing.uihelper.Constants; diff --git a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java index 09b855e2..d035874a 100644 --- a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java +++ b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java @@ -21,11 +21,11 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import android.graphics.Point; -import android.support.test.filters.MediumTest; import android.support.test.uiautomator.BySelector; import android.support.test.uiautomator.Direction; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; import com.android.tv.tests.ui.LiveChannelsTestController; diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml index 9b5df2ff..fa52946e 100644 --- a/tests/input/AndroidManifest.xml +++ b/tests/input/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.testinput"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <!-- Required to update or read existing channel and program information in TvProvider. --> <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> diff --git a/tests/input/jank.sh b/tests/input/jank.sh index c6311a4d..0710a4fc 100644 --- a/tests/input/jank.sh +++ b/tests/input/jank.sh @@ -17,8 +17,8 @@ # text fixture setup for unit tests -echo "text fixture setup for func tests" +echo "text fixture setup for jank tests" am instrument \ -e testSetupMode jank \ - -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
\ No newline at end of file + -w com.android.tv.testinput/.instrument.TestSetupInstrumentation diff --git a/tests/input/tools/get_test_logos.sh b/tests/input/tools/get_test_logos.sh index 4dd87a3a..649c51ae 100755 --- a/tests/input/tools/get_test_logos.sh +++ b/tests/input/tools/get_test_logos.sh @@ -25,7 +25,7 @@ icons=( bus cafe camping car-dealer car-rental car-repair casino caution cemetery-grave cemetery-tomb cinema civic-building computer corporate courthouse - fire flag floral helicopter home + fire flag helicopter home info landslide legal location locomotive medical mobile motorcycle music parking pet petrol phone picnic postal diff --git a/tests/jank/Android.mk b/tests/jank/Android.mk index 1b67ac3f..7df77ea0 100644 --- a/tests/jank/Android.mk +++ b/tests/jank/Android.mk @@ -10,7 +10,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PACKAGE_NAME := TVJankTests LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-test \ + androidx.test.runner \ tv-test-common \ ub-janktesthelper \ ub-uiautomator \ diff --git a/tests/jank/AndroidManifest.xml b/tests/jank/AndroidManifest.xml index 5ea72b44..7c0997ac 100644 --- a/tests/jank/AndroidManifest.xml +++ b/tests/jank/AndroidManifest.xml @@ -18,10 +18,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tests.jank" > - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" /> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" /> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" + android:name="androidx.test.runner.AndroidJUnitRunner" android:label="Live Channel Jank Tests" android:targetPackage="com.android.tv" /> diff --git a/tests/jank/README.md b/tests/jank/README.md new file mode 100644 index 00000000..c40eb229 --- /dev/null +++ b/tests/jank/README.md @@ -0,0 +1,32 @@ +# Jank tests for Live Channels + + +## AOSP instructions + +To run the jank tests + +```bash +echo "Compiling" +m -j LiveTv TVTestInput TVJankTests +echo "Installing" +adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk +adb install -r ${OUT}/system/app/TVTestInput/TVTestInput.apk +adb install -r ${OUT}/testcases/TVJankTests/TVJankTests.apk +echo "Setting up test input" +adb shell am instrument \ + -e testSetupMode jank \ + -w com.android.tv.testinput/.instrument.TestSetupInstrumentation +echo "Running the test" +adb shell am instrument \ + -w com.android.tv.tests.jank/android.support.test.runner.AndroidJUnitRunner + +``` + +If it is your first time installing LiveTv you will need to do + +```bash +adb root +adb remount +adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk +adb reboot +```
\ No newline at end of file diff --git a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java index eee2328b..02ca6730 100644 --- a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java +++ b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java @@ -15,9 +15,9 @@ */ package com.android.tv.tests.jank; -import android.support.test.filters.MediumTest; import android.support.test.jank.GfxMonitor; import android.support.test.jank.JankTest; +import androidx.test.filters.MediumTest; /** Jank tests for channel zapping. */ @MediumTest diff --git a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java index ea80eb3d..6b0dcd0e 100644 --- a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java +++ b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java @@ -15,9 +15,9 @@ */ package com.android.tv.tests.jank; -import android.support.test.filters.MediumTest; import android.support.test.jank.GfxMonitor; import android.support.test.jank.JankTest; +import androidx.test.filters.MediumTest; import com.android.tv.testing.uihelper.MenuHelper; /** Jank tests for the program guide. */ diff --git a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java index 57d38ba9..da2eb9cb 100644 --- a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java +++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java @@ -17,10 +17,10 @@ package com.android.tv.tests.jank; import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition; -import android.support.test.filters.MediumTest; import android.support.test.jank.GfxMonitor; import android.support.test.jank.JankTest; import android.support.test.uiautomator.Until; +import androidx.test.filters.MediumTest; import com.android.tv.R; import com.android.tv.testing.uihelper.ByResource; import com.android.tv.testing.uihelper.Constants; diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk index a425bcfe..5ea7ccd8 100644 --- a/tests/unit/Android.mk +++ b/tests/unit/Android.mk @@ -8,7 +8,7 @@ LOCAL_MODULE_TAGS := tests LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-test \ + androidx.test.runner \ mockito-target \ tv-test-common \ diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml index 9134a1c1..c7d2f528 100644 --- a/tests/unit/AndroidManifest.xml +++ b/tests/unit/AndroidManifest.xml @@ -18,10 +18,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tests" > - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23" /> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" /> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" + android:name="androidx.test.runner.AndroidJUnitRunner" android:label="Live Channel Unit Tests" android:targetPackage="com.android.tv" /> diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java index abadde31..4b85eaae 100644 --- a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java +++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java @@ -20,9 +20,9 @@ import static com.android.tv.TimeShiftManager.INVALID_TIME; import static com.android.tv.TimeShiftManager.REQUEST_TIMEOUT_MS; import static com.google.common.truth.Truth.assertWithMessage; -import android.support.test.annotation.UiThreadTest; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.annotation.UiThreadTest; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.testing.activities.BaseMainActivityTestCase; import org.junit.Before; import org.junit.Test; diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java index c5df21a9..f6223ec6 100644 --- a/tests/unit/src/com/android/tv/MainActivityTest.java +++ b/tests/unit/src/com/android/tv/MainActivityTest.java @@ -15,14 +15,14 @@ */ package com.android.tv; -import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; import android.view.View; import android.widget.TextView; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.data.api.Channel; import com.android.tv.testing.activities.BaseMainActivityTestCase; import com.android.tv.testing.testinput.TvTestInputConstants; diff --git a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java index cb523045..7adee385 100644 --- a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java +++ b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java @@ -24,8 +24,8 @@ import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY; import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND; import static com.google.common.truth.Truth.assertWithMessage; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.testing.activities.BaseMainActivityTestCase; import org.junit.Before; import org.junit.Test; diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java index 96c1f7a1..71ccaf35 100644 --- a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java +++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java @@ -16,8 +16,8 @@ package com.android.tv.data; -import static android.support.test.InstrumentationRegistry.getInstrumentation; -import static android.support.test.InstrumentationRegistry.getTargetContext; +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -25,14 +25,14 @@ import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; import android.database.Cursor; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.AsyncTask; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import android.os.Bundle; import android.test.MoreAsserts; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; @@ -40,10 +40,13 @@ import android.test.mock.MockCursor; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.data.api.Channel; import com.android.tv.testing.constants.Constants; import com.android.tv.testing.data.ChannelInfo; import com.android.tv.util.TvInputManagerHelper; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -98,9 +101,20 @@ public class ChannelDataManagerTest { Mockito.mock(TvInputManagerHelper.class); Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString())) .thenReturn(true); + Context mockContext = Mockito.mock(Context.class); + Mockito.when(mockContext.getContentResolver()) + .thenReturn(mContentResolver); + Mockito.when(mockContext.checkSelfPermission(Matchers.anyString())) + .thenAnswer( + invocation -> { + Object[] args = invocation.getArguments(); + return getTargetContext() + .checkSelfPermission(((String) args[0])); + }); + mChannelDataManager = new ChannelDataManager( - getTargetContext(), + mockContext, mockHelper, AsyncTask.SERIAL_EXECUTOR, mContentResolver); @@ -417,6 +431,15 @@ public class ChannelDataManagerTest { } } + @Override + public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) { + try { + return getTargetContext().getContentResolver().openAssetFileDescriptor(url, "r"); + } catch (FileNotFoundException e) { + return null; + } + } + /** * Implementation of {@link ContentProvider#query}. This assumes that {@link * ChannelDataManager} queries channels with empty {@code selection}. (i.e. channels are diff --git a/tests/unit/src/com/android/tv/data/ChannelImplTest.java b/tests/unit/src/com/android/tv/data/ChannelImplTest.java index b791a7e4..86cfab66 100644 --- a/tests/unit/src/com/android/tv/data/ChannelImplTest.java +++ b/tests/unit/src/com/android/tv/data/ChannelImplTest.java @@ -25,8 +25,8 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.data.api.Channel; import com.android.tv.testing.ComparatorTester; import com.android.tv.util.TvInputManagerHelper; diff --git a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java b/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java deleted file mode 100644 index 8e892cce..00000000 --- a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2015 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.data; - -import android.content.pm.ResolveInfo; -import android.media.tv.TvInputInfo; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import android.util.Pair; -import com.android.tv.testing.ComparatorTester; -import com.android.tv.testing.utils.TestUtils; -import com.android.tv.util.SetupUtils; -import com.android.tv.util.TvInputManagerHelper; -import java.util.Comparator; -import java.util.LinkedHashMap; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Matchers; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** Test for {@link TvInputNewComparator} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class TvInputNewComparatorTest { - @Test - public void testComparator() throws Exception { - LinkedHashMap<String, Pair<Boolean, Boolean>> inputIdToNewInput = new LinkedHashMap<>(); - inputIdToNewInput.put("2_new_input", new Pair<>(true, false)); - inputIdToNewInput.put("4_new_input", new Pair<>(true, false)); - inputIdToNewInput.put("4_old_input", new Pair<>(false, false)); - inputIdToNewInput.put("0_old_input", new Pair<>(false, true)); - inputIdToNewInput.put("1_old_input", new Pair<>(false, true)); - inputIdToNewInput.put("3_old_input", new Pair<>(false, true)); - - SetupUtils setupUtils = Mockito.mock(SetupUtils.class); - Mockito.when(setupUtils.isNewInput(Matchers.anyString())) - .thenAnswer( - new Answer<Boolean>() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - String inputId = (String) invocation.getArguments()[0]; - return inputIdToNewInput.get(inputId).first; - } - }); - Mockito.when(setupUtils.isSetupDone(Matchers.anyString())) - .thenAnswer( - new Answer<Boolean>() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - String inputId = (String) invocation.getArguments()[0]; - return inputIdToNewInput.get(inputId).second; - } - }); - TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class); - Mockito.when(inputManager.getDefaultTvInputInfoComparator()) - .thenReturn( - new Comparator<TvInputInfo>() { - @Override - public int compare(TvInputInfo lhs, TvInputInfo rhs) { - return lhs.getId().compareTo(rhs.getId()); - } - }); - TvInputNewComparator comparator = new TvInputNewComparator(setupUtils, inputManager); - ComparatorTester<TvInputInfo> comparatorTester = - ComparatorTester.withoutEqualsTest(comparator); - ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test"); - for (String id : inputIdToNewInput.keySet()) { - // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString} - TvInputInfo info1 = - TestUtils.createTvInputInfo( - resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false); - TvInputInfo info2 = - TestUtils.createTvInputInfo( - resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true); - TvInputInfo info3 = - TestUtils.createTvInputInfo( - resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true); - comparatorTester.addComparableGroup(info1, info2, info3); - } - comparatorTester.test(); - } -} diff --git a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java b/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java deleted file mode 100644 index 43bfde09..00000000 --- a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2015 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.data; - -import static android.support.test.InstrumentationRegistry.getTargetContext; -import static com.google.common.truth.Truth.assertThat; - -import android.os.Looper; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; -import com.android.tv.data.WatchedHistoryManager.WatchedRecord; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test for {@link com.android.tv.data.WatchedHistoryManagerTest} - * - * <p>This is a medium test because it load files which accessing SharedPreferences. - */ -@MediumTest -@RunWith(AndroidJUnit4.class) -public class WatchedHistoryManagerTest { - // Wait time for expected success. - private static final int MAX_HISTORY_SIZE = 100; - - private WatchedHistoryManager mWatchedHistoryManager; - private TestWatchedHistoryManagerListener mListener; - - @Before - public void setUp() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mWatchedHistoryManager = new WatchedHistoryManager(getTargetContext(), MAX_HISTORY_SIZE); - mListener = new TestWatchedHistoryManagerListener(); - mWatchedHistoryManager.setListener(mListener); - } - - private void startAndWaitForComplete() throws InterruptedException { - mWatchedHistoryManager.start(); - assertThat(mListener.mLoadFinished).isTrue(); - } - - @Test - public void testIsLoaded() throws InterruptedException { - startAndWaitForComplete(); - assertThat(mWatchedHistoryManager.isLoaded()).isTrue(); - } - - @Test - public void testLogChannelViewStop() throws InterruptedException { - startAndWaitForComplete(); - long fakeId = 100000000; - long time = System.currentTimeMillis(); - long duration = TimeUnit.MINUTES.toMillis(10); - ChannelImpl channel = new ChannelImpl.Builder().setId(fakeId).build(); - mWatchedHistoryManager.logChannelViewStop(channel, time, duration); - - WatchedRecord record = mWatchedHistoryManager.getRecord(0); - WatchedRecord recordFromSharedPreferences = - mWatchedHistoryManager.getRecordFromSharedPreferences(0); - assertThat(fakeId).isEqualTo(record.channelId); - assertThat(time - duration).isEqualTo(record.watchedStartTime); - assertThat(duration).isEqualTo(record.duration); - assertThat(recordFromSharedPreferences).isEqualTo(record); - } - - @Test - public void testCircularHistoryQueue() throws InterruptedException { - startAndWaitForComplete(); - final long startChannelId = 100000000; - long time = System.currentTimeMillis(); - long duration = TimeUnit.MINUTES.toMillis(10); - - int size = MAX_HISTORY_SIZE * 2; - for (int i = 0; i < size; ++i) { - ChannelImpl channel = new ChannelImpl.Builder().setId(startChannelId + i).build(); - mWatchedHistoryManager.logChannelViewStop(channel, time + duration * i, duration); - } - for (int i = 0; i < MAX_HISTORY_SIZE; ++i) { - WatchedRecord record = mWatchedHistoryManager.getRecord(i); - WatchedRecord recordFromSharedPreferences = - mWatchedHistoryManager.getRecordFromSharedPreferences(i); - assertThat(recordFromSharedPreferences).isEqualTo(record); - assertThat(startChannelId + size - 1 - i).isEqualTo(record.channelId); - } - // Since the WatchedHistory is a circular queue, the value for 0 and maxHistorySize - // are same. - assertThat(mWatchedHistoryManager.getRecordFromSharedPreferences(MAX_HISTORY_SIZE)) - .isEqualTo(mWatchedHistoryManager.getRecordFromSharedPreferences(0)); - } - - @Test - public void testWatchedRecordEquals() { - assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 3))).isTrue(); - assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 4))).isFalse(); - assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 4, 3))).isFalse(); - assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(4, 2, 3))).isFalse(); - } - - @Test - public void testEncodeDecodeWatchedRecord() { - long fakeId = 100000000; - long time = System.currentTimeMillis(); - long duration = TimeUnit.MINUTES.toMillis(10); - WatchedRecord record = new WatchedRecord(fakeId, time, duration); - WatchedRecord sameRecord = - mWatchedHistoryManager.decode(mWatchedHistoryManager.encode(record)); - assertThat(sameRecord).isEqualTo(record); - } - - private static final class TestWatchedHistoryManagerListener - implements WatchedHistoryManager.Listener { - boolean mLoadFinished; - - @Override - public void onLoadFinished() { - mLoadFinished = true; - } - - @Override - public void onNewRecordAdded(WatchedRecord watchedRecord) {} - } -} diff --git a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java index d510da32..546f074f 100644 --- a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java +++ b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java @@ -20,9 +20,9 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Intent; import android.os.Build; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; import android.test.ServiceTestCase; +import androidx.test.filters.SdkSuppress; +import androidx.test.filters.SmallTest; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.feature.TestableFeature; import org.mockito.MockitoAnnotations; diff --git a/tests/unit/src/com/android/tv/FeaturesTest.java b/tests/unit/src/com/android/tv/features/FeaturesTest.java index e19f4b7c..e35758c3 100644 --- a/tests/unit/src/com/android/tv/FeaturesTest.java +++ b/tests/unit/src/com/android/tv/features/FeaturesTest.java @@ -14,12 +14,12 @@ * limitations under the License */ -package com.android.tv; +package com.android.tv.features; import static com.google.common.truth.Truth.assertThat; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/unit/src/com/android/tv/menu/MenuTest.java b/tests/unit/src/com/android/tv/menu/MenuTest.java index 028a185d..e384c398 100644 --- a/tests/unit/src/com/android/tv/menu/MenuTest.java +++ b/tests/unit/src/com/android/tv/menu/MenuTest.java @@ -15,11 +15,11 @@ */ package com.android.tv.menu; -import static android.support.test.InstrumentationRegistry.getTargetContext; +import static androidx.test.InstrumentationRegistry.getTargetContext; import static com.google.common.truth.Truth.assertWithMessage; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.menu.Menu.OnMenuVisibilityChangeListener; import org.junit.Before; import org.junit.Ignore; diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java index 0f815a7a..5ecbdf02 100644 --- a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java +++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java @@ -15,15 +15,15 @@ */ package com.android.tv.menu; -import static android.support.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import android.media.tv.TvTrackInfo; import android.os.SystemClock; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; import android.text.TextUtils; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.testing.activities.BaseMainActivityTestCase; import com.android.tv.testing.constants.Constants; import com.android.tv.testing.testinput.ChannelState; diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java index e63bdc3a..c217222f 100644 --- a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java +++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java @@ -16,11 +16,11 @@ package com.android.tv.recommendation; -import static android.support.test.InstrumentationRegistry.getContext; +import static androidx.test.InstrumentationRegistry.getContext; import static com.google.common.truth.Truth.assertThat; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.testing.utils.Utils; import java.util.Random; import java.util.concurrent.TimeUnit; diff --git a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java index f62a5e05..9696d8b6 100644 --- a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java +++ b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java @@ -16,7 +16,7 @@ package com.android.tv.recommendation; -import static android.support.test.InstrumentationRegistry.getContext; +import static androidx.test.InstrumentationRegistry.getContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java index e14320f0..773d3358 100644 --- a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java @@ -18,8 +18,8 @@ package com.android.tv.recommendation; import static com.google.common.truth.Truth.assertThat; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.Test; diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java index f8d6b220..15a3726a 100644 --- a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java @@ -18,8 +18,8 @@ package com.android.tv.recommendation; import static com.google.common.truth.Truth.assertThat; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java index 812a3eb1..01208d27 100644 --- a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java @@ -16,12 +16,12 @@ package com.android.tv.recommendation; -import static android.support.test.InstrumentationRegistry.getContext; +import static androidx.test.InstrumentationRegistry.getContext; import static com.google.common.truth.Truth.assertThat; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; import android.test.MoreAsserts; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.data.api.Channel; import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper; import com.android.tv.testing.utils.Utils; diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java index 39e6e9c5..d9149050 100644 --- a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java @@ -19,8 +19,8 @@ package com.android.tv.recommendation; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.data.Program; import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime; import java.util.Calendar; diff --git a/tests/unit/src/com/android/tv/util/MockTvSingletons.java b/tests/unit/src/com/android/tv/util/MockTvSingletons.java index 6de1eb3e..fd4b43cf 100644 --- a/tests/unit/src/com/android/tv/util/MockTvSingletons.java +++ b/tests/unit/src/com/android/tv/util/MockTvSingletons.java @@ -17,16 +17,19 @@ package com.android.tv.util; import android.content.Context; -import android.content.Intent; import com.android.tv.InputSessionManager; import com.android.tv.MainActivityWrapper; import com.android.tv.TvApplication; import com.android.tv.TvSingletons; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; -import com.android.tv.common.config.api.RemoteConfig; import com.android.tv.common.experiments.ExperimentLoader; +import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags; +import com.android.tv.common.flags.impl.DefaultCloudEpgFlags; +import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags; +import com.android.tv.common.flags.impl.DefaultUiFlags; import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.common.util.Clock; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.PreviewDataManager; @@ -40,15 +43,21 @@ import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.perf.PerformanceMonitor; import com.android.tv.testing.FakeClock; -import com.android.tv.tuner.TunerInputController; +import com.android.tv.tunerinputcontroller.BuiltInTunerManager; +import com.google.common.base.Optional; import java.util.concurrent.Executor; import javax.inject.Provider; /** Mock {@link TvSingletons} class. */ -public class MockTvSingletons implements TvSingletons { +public class MockTvSingletons implements TvSingletons, HasSingletons<TvSingletons> { public final FakeClock fakeClock = FakeClock.createWithCurrentTime(); private final TvApplication mApp; + private final DefaultBackendKnobsFlags mBackendFlags = new DefaultBackendKnobsFlags(); + private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags(); + private final DefaultUiFlags mUiFlags = new DefaultUiFlags(); + private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags = + new DefaultConcurrentDvrPlaybackFlags(); private PerformanceMonitor mPerformanceMonitor; public MockTvSingletons(Context context) { @@ -154,8 +163,8 @@ public class MockTvSingletons implements TvSingletons { } @Override - public TunerInputController getTunerInputController() { - return mApp.getTunerInputController(); + public Optional<BuiltInTunerManager> getBuiltInTunerManager() { + return mApp.getBuiltInTunerManager(); } @Override @@ -174,16 +183,6 @@ public class MockTvSingletons implements TvSingletons { } @Override - public RemoteConfig getRemoteConfig() { - return mApp.getRemoteConfig(); - } - - @Override - public Intent getTunerSetupIntent(Context context) { - return mApp.getTunerSetupIntent(context); - } - - @Override public boolean isRunningInMainProcess() { return mApp.isRunningInMainProcess(); } @@ -198,12 +197,37 @@ public class MockTvSingletons implements TvSingletons { } @Override - public String getEmbeddedTunerInputId() { - return "com.android.tv/.tuner.tvinput.LiveTvTunerTvInputService"; + public DefaultCloudEpgFlags getCloudEpgFlags() { + return mCloudEpgFlags; + } + + @Override + public DefaultUiFlags getUiFlags() { + return mUiFlags; } @Override public Executor getDbExecutor() { return mApp.getDbExecutor(); } + + @Override + public DefaultBackendKnobsFlags getBackendKnobs() { + return mBackendFlags; + } + + @Override + public BuildType getBuildType() { + return BuildType.ENG; + } + + @Override + public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() { + return mConcurrentDvrPlaybackFlags; + } + + @Override + public TvSingletons singletons() { + return this; + } } diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java index 6dfed64a..7e35d76b 100644 --- a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java +++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java @@ -16,12 +16,12 @@ package com.android.tv.util; -import static android.support.test.InstrumentationRegistry.getContext; +import static androidx.test.InstrumentationRegistry.getContext; import android.content.pm.ResolveInfo; import android.media.tv.TvInputInfo; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.testing.ComparatorTester; import com.android.tv.testing.utils.TestUtils; import java.util.ArrayList; diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java deleted file mode 100644 index d84a90d4..00000000 --- a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2015 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.util; - -import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo; -import static com.google.common.truth.Truth.assertWithMessage; - -import android.media.tv.TvTrackInfo; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import com.android.tv.testing.ComparatorTester; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link com.android.tv.util.TvTrackInfoUtils}. */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class TvTrackInfoUtilsTest { - private static final String UN_MATCHED_ID = "no matching ID"; - - private static final TvTrackInfo INFO_1_EN_1 = create("1", "en", 1); - - private static final TvTrackInfo INFO_2_EN_5 = create("2", "en", 5); - - private static final TvTrackInfo INFO_3_FR_8 = create("3", "fr", 8); - - private static final TvTrackInfo INFO_4_NULL_2 = create("4", null, 2); - - private static final TvTrackInfo INFO_5_NULL_6 = create("5", null, 6); - - private static TvTrackInfo create(String id, String fr, int audioChannelCount) { - return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id) - .setLanguage(fr) - .setAudioChannelCount(audioChannelCount) - .build(); - } - - private static final List<TvTrackInfo> ALL = - Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_8, INFO_4_NULL_2, INFO_5_NULL_6); - private static final List<TvTrackInfo> NULL_LANGUAGE_TRACKS = - Arrays.asList(INFO_4_NULL_2, INFO_5_NULL_6); - - @Test - public void testGetBestTrackInfo_empty() { - TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), UN_MATCHED_ID, "en", 1); - assertWithMessage("best track ").that(result).isEqualTo(null); - } - - @Test - public void testGetBestTrackInfo_exactMatch() { - TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1); - assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); - } - - @Test - public void testGetBestTrackInfo_langAndChannelCountMatch() { - TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5); - assertWithMessage("best track ").that(result).isEqualTo(INFO_2_EN_5); - } - - @Test - public void testGetBestTrackInfo_languageOnlyMatch() { - TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1); - assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8); - } - - @Test - public void testGetBestTrackInfo_channelCountOnlyMatchWithNullLanguage() { - TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 8); - assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8); - } - - @Test - public void testGetBestTrackInfo_noMatches() { - TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1); - assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); - } - - @Test - public void testGetBestTrackInfo_noMatchesWithNullLanguage() { - TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 0); - assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); - } - - @Test - public void testGetBestTrackInfo_channelCountAndIdMatch() { - TvTrackInfo result = getBestTrackInfo(NULL_LANGUAGE_TRACKS, "5", null, 6); - assertWithMessage("best track ").that(result).isEqualTo(INFO_5_NULL_6); - } - - @Test - public void testComparator() { - Comparator<TvTrackInfo> comparator = TvTrackInfoUtils.createComparator("1", "en", 1); - ComparatorTester.withoutEqualsTest(comparator) - // lang not match - .addComparableGroup( - create("1", "kr", 1), - create("2", "kr", 2), - create("1", "ja", 1), - create("1", "ch", 1)) - // lang match not count match - .addComparableGroup( - create("2", "en", 2), create("3", "en", 3), create("1", "en", 2)) - // lang and count match - .addComparableGroup(create("2", "en", 1), create("3", "en", 1)) - // all match - .addComparableGroup(create("1", "en", 1), create("1", "en", 1)) - .test(); - } -} diff --git a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java index b7715c4a..41722135 100644 --- a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java +++ b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java @@ -20,8 +20,8 @@ import static com.android.tv.util.images.BitmapUtils.createScaledBitmapInfo; import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Bitmap; -import android.support.test.filters.MediumTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; import org.junit.Before; import org.junit.Test; diff --git a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java index 005775b6..1bb650fb 100644 --- a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java +++ b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java @@ -18,8 +18,8 @@ package com.android.tv.util.images; import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Bitmap; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tuner/Android.bp b/tuner/Android.bp new file mode 100644 index 00000000..215a1e53 --- /dev/null +++ b/tuner/Android.bp @@ -0,0 +1,48 @@ +// +// Copyright (C) 2019 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. +// + +android_library { + name: "live-tv-tuner", + srcs: ["src/**/*.java"], + sdk_version: "system_current", + resource_dirs: ["res"], + libs: [ + "tv-auto-value-jar", + "tv-auto-factory-jar", + "android-support-annotations", + "tv-error-prone-annotations-jar", + "tv-guava-android-jar", + "tv-javax-annotations-jar", + "jsr330", + "tv-lib-dagger", + "tv-lib-exoplayer", + "tv-lib-exoplayer-v2-core", + "live-tv-tuner-proto", + "android-support-compat", + "android-support-core-ui", + "android-support-v7-palette", + "android-support-v7-recyclerview", + "android-support-v17-leanback", + "androidx.tvprovider_tvprovider", + "tv-lib-dagger-android", + "tv-common", + ], + plugins: [ + "tv-auto-value", + "tv-auto-factory", + ], + min_sdk_version: "23", +} diff --git a/tuner/Android.mk b/tuner/Android.mk deleted file mode 100644 index aedda3c1..00000000 --- a/tuner/Android.mk +++ /dev/null @@ -1,44 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# Include all java and proto files. -LOCAL_SRC_FILES := \ - $(call all-java-files-under, src) \ - $(call all-proto-files-under, proto) - - -LOCAL_MODULE := live-tv-tuner -LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES -LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := system_current - -LOCAL_USE_AAPT2 := true - -LOCAL_PROTOC_OPTIMIZE_TYPE := nano -LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/proto/ -LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java - -LOCAL_RESOURCE_DIR := \ - $(LOCAL_PATH)/res \ - -LOCAL_JAVA_LIBRARIES := \ - android-support-annotations \ - lib-exoplayer \ - lib-exoplayer-v2-core \ - -LOCAL_SHARED_ANDROID_LIBRARIES := \ - android-support-compat \ - android-support-core-ui \ - android-support-tv-provider \ - android-support-v7-palette \ - android-support-v7-recyclerview \ - android-support-v17-leanback \ - android-support-tv-provider \ - tv-common \ - -LOCAL_MIN_SDK_VERSION := 23 - -include $(LOCAL_PATH)/buildconfig.mk - -include $(BUILD_STATIC_JAVA_LIBRARY) - diff --git a/tuner/AndroidManifest.xml b/tuner/AndroidManifest.xml index af80f692..fd217717 100644 --- a/tuner/AndroidManifest.xml +++ b/tuner/AndroidManifest.xml @@ -15,8 +15,10 @@ ~ 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.tuner" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/> - <application /> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> + <application tools:replace="android:appComponentFactory" + android:appComponentFactory="android.support.v4.app.CoreComponentFactory" /> </manifest> diff --git a/tuner/BuildConfig.java.in b/tuner/BuildConfig.java.in deleted file mode 100644 index 85967fad..00000000 --- a/tuner/BuildConfig.java.in +++ /dev/null @@ -1,8 +0,0 @@ -/* This file is auto generated. Do not modify. */ -package com.android.tv.tuner; - -public final class BuildConfig { - public static final boolean DEBUG = %DEBUG%; - public static final boolean ENG = %ENG%; - private BuildConfig() {} -}
\ No newline at end of file diff --git a/tuner/SampleDvbTuner/AndroidManifest.xml b/tuner/SampleDvbTuner/AndroidManifest.xml index 740989ad..5ad927e3 100755 --- a/tuner/SampleDvbTuner/AndroidManifest.xml +++ b/tuner/SampleDvbTuner/AndroidManifest.xml @@ -19,7 +19,7 @@ <uses-sdk android:minSdkVersion="23" - android:targetSdkVersion="26" /> + android:targetSdkVersion="27" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> @@ -50,12 +50,10 @@ <application android:name="com.android.tv.tuner.sample.dvb.app.SampleDvbTuner" + android:appComponentFactory="android.support.v4.app.CoreComponentFactory" android:icon="@mipmap/ic_launcher" android:label="@string/sample_dvb_tuner_app_name" > - <activity - android:name="com.google.android.gms.common.api.GoogleApiActivity" - android:exported="false" - android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + <activity android:name="com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity" @@ -73,7 +71,7 @@ android:name="com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService" android:label="@string/sample_dvb_tuner_app_name" android:permission="android.permission.BIND_TV_INPUT" - android:process="com.google.android.tv.tuner.sample.dvb.tvinput" > + android:process="com.android.tv.tuner.sample.dvb.tvinput" > <intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> diff --git a/tuner/SampleDvbTuner/ResourceManifest.xml b/tuner/SampleDvbTuner/ResourceManifest.xml deleted file mode 100644 index e13f9584..00000000 --- a/tuner/SampleDvbTuner/ResourceManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2018 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" - package="com.android.tv.tuner.sample.dvb" xmlns:tools="http://schemas.android.com/tools"> - - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> - - <!-- Only used for resources--> -</manifest> diff --git a/tuner/SampleDvbTuner/build.gradle b/tuner/SampleDvbTuner/build.gradle new file mode 100644 index 00000000..657a4258 --- /dev/null +++ b/tuner/SampleDvbTuner/build.gradle @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +buildscript { + repositories { + mavenCentral() + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5' + } +} +apply plugin: 'com.android.application' +apply plugin: 'com.google.protobuf' +android { + compileSdkVersion 26 + buildToolsVersion '28.0.2' + + dexOptions { + preDexLibraries = false + additionalParameters=['--core-library'] + javaMaxHeapSize "6g" + } + android { + defaultConfig { + resConfigs "en" + } + } + defaultConfig { + minSdkVersion 23 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + minifyEnabled false + } + } + compileOptions() { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src', '../../partner_support/src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +repositories { + mavenCentral() + jcenter() + google() +} + +final String SUPPORT_LIBS_VERSION = '26.1.0' +dependencies { + implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0' + implementation 'com.google.android.exoplayer:exoplayer:r1.5.16' + implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}" + /*Not building with latest one (1.6.2)*/ + annotationProcessor 'com.google.auto.value:auto-value:1.5.4' + implementation 'com.google.auto.value:auto-value:1.5.4' + implementation project(':common') + implementation project(':tuner') +} diff --git a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png Binary files differindex b5c51706..9735fec5 100644 --- a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png +++ b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png diff --git a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png Binary files differindex 66297216..3bb94802 100644 --- a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png +++ b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png diff --git a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png Binary files differindex f259d1c9..c1c9c73f 100644 --- a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png +++ b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png diff --git a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png Binary files differindex 421cd08b..0556c2c1 100644 --- a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png +++ b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png diff --git a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png Binary files differindex 91be3220..652fc456 100644 --- a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png +++ b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/tuner/SampleDvbTuner/settings.gradle b/tuner/SampleDvbTuner/settings.gradle new file mode 100644 index 00000000..13cb90f8 --- /dev/null +++ b/tuner/SampleDvbTuner/settings.gradle @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +include ':common' +include ':tuner' +project(":common").projectDir = file("../../common") +project(":tuner").projectDir = file(".././") diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml index adb8e309..dc042286 100644 --- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml @@ -36,6 +36,10 @@ <uses-feature android:name="android.software.leanback" android:required="true" /> <uses-feature android:name="android.software.live_tv" android:required="true" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> - <application /> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> + <application + android:name=".app.SampleDvbTuner" + android:icon="@mipmap/ic_launcher" + android:label="@string/sample_dvb_tuner_app_name" + /> </manifest> diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java index 15e90437..568e3c98 100644 --- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java @@ -17,30 +17,39 @@ package com.android.tv.tuner.sample.dvb.app; import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; import android.media.tv.TvContract; import com.android.tv.common.BaseApplication; -import com.android.tv.common.actions.InputSetupActionUtils; -import com.android.tv.common.config.DefaultConfigManager; -import com.android.tv.common.config.api.RemoteConfig; -import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.singletons.HasSingletons; +import com.android.tv.tuner.modules.TunerSingletonsModule; +import com.android.tv.tuner.sample.dvb.singletons.SampleDvbSingletons; import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService; -import com.android.tv.tuner.setup.LiveTvTunerSetupActivity; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl; +import dagger.android.AndroidInjector; +import com.android.tv.common.flags.CloudEpgFlags; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; +import javax.inject.Inject; /** The top level application for Sample DVB Tuner. */ -public class SampleDvbTuner extends BaseApplication { +public class SampleDvbTuner extends BaseApplication + implements SampleDvbSingletons, HasSingletons<SampleDvbSingletons> { + private String mEmbeddedInputId; - private RemoteConfig mRemoteConfig; + @Inject CloudEpgFlags mCloudEpgFlags; + @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + @Inject TunerSessionFactoryImpl mTunerSessionFactory; + + @Override + public void onCreate() { + super.onCreate(); + } @Override - public Intent getTunerSetupIntent(Context context) { - // Make an intent to launch the setup activity of TV tuner input. - Intent intent = - CommonUtils.createSetupIntent( - new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId); - intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId); - return intent; + protected AndroidInjector<SampleDvbTuner> applicationInjector() { + return DaggerSampleDvbTunerComponent.builder() + .sampleDvbTunerModule(new SampleDvbTunerModule(this)) + .tunerSingletonsModule(new TunerSingletonsModule(this)) + .build(); } @Override @@ -54,11 +63,26 @@ public class SampleDvbTuner extends BaseApplication { } @Override - public RemoteConfig getRemoteConfig() { - if (mRemoteConfig == null) { - // No need to synchronize this, it does not hurt to create two and throw one away. - mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig(); - } - return mRemoteConfig; + public CloudEpgFlags getCloudEpgFlags() { + return mCloudEpgFlags; + } + + @Override + public BuildType getBuildType() { + return BuildType.ENG; + } + + @Override + public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() { + return mConcurrentDvrPlaybackFlags; + } + + @Override + public SampleDvbSingletons singletons() { + return this; + } + + public TunerSessionFactory getTunerSessionFactory() { + return mTunerSessionFactory; } } diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java new file mode 100644 index 00000000..e6c80ea9 --- /dev/null +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.tuner.sample.dvb.app; + +import dagger.Component; +import dagger.android.AndroidInjectionModule; +import dagger.android.AndroidInjector; +import javax.inject.Singleton; + +/** Dagger component for {@link SampleDvbTuner}. */ +@Singleton +@Component(modules = {AndroidInjectionModule.class, SampleDvbTunerModule.class}) +public interface SampleDvbTunerComponent extends AndroidInjector<SampleDvbTuner> {} diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java new file mode 100644 index 00000000..4da3ca9d --- /dev/null +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.tuner.sample.dvb.app; + +import com.android.tv.common.flags.impl.DefaultFlagsModule; +import com.android.tv.tuner.api.TunerFactory; +import com.android.tv.tuner.builtin.BuiltInTunerHalFactory; +import com.android.tv.tuner.modules.TunerModule; +import com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity; +import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import dagger.Module; +import dagger.Provides; + +/** Dagger module for {@link SampleDvbTuner}. */ +@Module( + includes = { + DefaultFlagsModule.class, + SampleDvbTunerTvInputService.Module.class, + SampleDvbTunerSetupActivity.Module.class, + TunerModule.class, + }) +class SampleDvbTunerModule { + private final SampleDvbTuner mSampleDvbTuner; + + SampleDvbTunerModule(SampleDvbTuner sampleDvbTuner) { + mSampleDvbTuner = sampleDvbTuner; + } + + @Provides + public TunerSessionFactory providesTunerSessionFactory() { + return mSampleDvbTuner.getTunerSessionFactory(); + } + + @Provides + TunerFactory providesTunerFactory() { + return BuiltInTunerHalFactory.INSTANCE; + } +} diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java index 54b3a9e7..f9ef29c4 100644 --- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java @@ -20,6 +20,7 @@ import android.app.FragmentManager; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; +import android.content.pm.PackageManager; import android.media.tv.TvInputInfo; import android.os.AsyncTask; import android.os.Bundle; @@ -29,26 +30,28 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; -import com.android.tv.common.BaseApplication; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.singletons.HasSingletons; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.common.util.PostalCodeUtils; -import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.sample.dvb.R; import com.android.tv.tuner.setup.BaseTunerSetupActivity; import com.android.tv.tuner.setup.ConnectionTypeFragment; import com.android.tv.tuner.setup.LineupFragment; +import com.android.tv.tuner.setup.LocationFragment; import com.android.tv.tuner.setup.PostalCodeFragment; import com.android.tv.tuner.setup.ScanFragment; import com.android.tv.tuner.setup.ScanResultFragment; import com.android.tv.tuner.setup.WelcomeFragment; +import com.android.tv.tuner.singletons.TunerSingletons; import com.google.android.tv.partner.support.EpgContract; import com.google.android.tv.partner.support.EpgInput; import com.google.android.tv.partner.support.EpgInputs; import com.google.android.tv.partner.support.Lineup; import com.google.android.tv.partner.support.Lineups; import com.google.android.tv.partner.support.TunerSetupUtils; +import dagger.android.ContributesAndroidInjector; import java.util.ArrayList; import java.util.List; @@ -72,23 +75,19 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { private EpgInput epgInput; private String postalCode; private final Handler handler = new Handler(); - private final Runnable cancelFetchLineupTaskRunnable = - new Runnable() { - @Override - public void run() { - cancelFetchLineup(); - } - }; + private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup; private String embeddedInputId; @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); if (DEBUG) { Log.d(TAG, "onCreate"); } - embeddedInputId = BaseApplication.getSingletons(this).getEmbeddedTunerInputId(); + embeddedInputId = + HasSingletons.get(TunerSingletons.class, getApplicationContext()) + .getEmbeddedTunerInputId(); new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - super.onCreate(savedInstanceState); } @Override @@ -96,7 +95,7 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... arg0) { - return TunerHal.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first; + return mTunerFactory.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first; } @Override @@ -125,10 +124,16 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { break; default: String postalCode = PostalCodeUtils.getLastPostalCode(this); - if (mNeedToShowPostalCodeFragment - || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( + boolean needLocation = + CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( getApplicationContext()) - && TextUtils.isEmpty(postalCode))) { + && TextUtils.isEmpty(postalCode); + if (needLocation + && checkSelfPermission( + android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + showLocationFragment(); + } else if (mNeedToShowPostalCodeFragment || needLocation) { // We cannot get postal code automatically. Postal code input fragment // should always be shown even if users have input some valid postal // code in this activity before. @@ -144,6 +149,26 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { break; } return true; + case LocationFragment.ACTION_CATEGORY: + switch (actionId) { + case LocationFragment.ACTION_ALLOW_PERMISSION: + String postalCode = + params == null + ? null + : params.getString(LocationFragment.KEY_POSTAL_CODE); + if (postalCode == null) { + showPostalCodeFragment(); + } else { + this.postalCode = postalCode; + restartFetchLineupTask(); + showConnectionTypeFragment(); + } + break; + default: + cancelFetchLineup(); + showConnectionTypeFragment(); + } + return true; case PostalCodeFragment.ACTION_CATEGORY: lineups = null; selectedLineup = null; @@ -220,8 +245,7 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { case ScanResultFragment.ACTION_CATEGORY: switch (actionId) { case SetupMultiPaneFragment.ACTION_DONE: - new SampleDvbTunerSetupActivity.InsertOrModifyEpgInputTask( - selectedLineup, embeddedInputId) + new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); break; default: @@ -340,7 +364,9 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { private void restartFetchLineupTask() { if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext()) - || TextUtils.isEmpty(postalCode)) { + || TextUtils.isEmpty(postalCode) + || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { return; } if (fetchLineupTask != null) { @@ -441,6 +467,16 @@ public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity { } } + /** + * Exports {@link SampleDvbTunerSetupActivity} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract SampleDvbTunerSetupActivity contributeSampleDvbTunerSetupActivityInjector(); + } + private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> { private final String inputId; diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java new file mode 100644 index 00000000..22a67905 --- /dev/null +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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.tuner.sample.dvb.singletons; + +import com.android.tv.common.BaseSingletons; +import com.android.tv.tuner.singletons.TunerSingletons; + +/** Singletons for SampleDvbTuner. */ +public interface SampleDvbSingletons extends BaseSingletons, TunerSingletons {} diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java index ae15affc..a31faa81 100644 --- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java +++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java @@ -16,6 +16,18 @@ package com.android.tv.tuner.sample.dvb.tvinput; import com.android.tv.tuner.tvinput.BaseTunerTvInputService; +import dagger.android.ContributesAndroidInjector; /** Sample DVB Tuner {@link android.media.tv.TvInputService}. */ -public class SampleDvbTunerTvInputService extends BaseTunerTvInputService {} +public class SampleDvbTunerTvInputService extends BaseTunerTvInputService { + + /** + * Exports {@link SampleDvbTunerTvInputService} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract SampleDvbTunerTvInputService contributesSampleDvbTunerTvInputServiceInjector(); + } +} diff --git a/tuner/SampleNetworkTuner/AndroidManifest.xml b/tuner/SampleNetworkTuner/AndroidManifest.xml new file mode 100755 index 00000000..0ec9afca --- /dev/null +++ b/tuner/SampleNetworkTuner/AndroidManifest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 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" + package="com.android.tv.tuner.sample.network" > + + <uses-sdk + android:minSdkVersion="23" + android:targetSdkVersion="27" /> + + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <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" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> + <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> + + <!-- Permissions/feature for USB tuner --> + <uses-permission android:name="android.permission.DVB_DEVICE" /> + + <uses-feature + android:name="android.hardware.usb.host" + android:required="false" /> + + <!-- Limit only for Android TV --> + <uses-feature + android:name="android.software.leanback" + android:required="true" /> + <uses-feature + android:name="android.software.live_tv" + android:required="true" /> + <uses-feature + android:name="android.hardware.touchscreen" + android:required="false" /> + + <application + android:name="com.android.tv.tuner.sample.network.app.SampleNetworkTuner" + android:appComponentFactory="android.support.v4.app.CoreComponentFactory" + android:icon="@mipmap/ic_launcher" + android:label="@string/sample_network_tuner_app_name" > + + + <activity + android:name="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity" + android:configChanges="keyboard|keyboardHidden" + android:exported="true" + android:label="@string/sample_network_tuner_app_name" + android:launchMode="singleInstance" + android:theme="@style/Theme.Setup.GuidedStep" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + </intent-filter> + </activity> + + <service + android:name="com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService" + android:label="@string/sample_network_tuner_app_name" + android:permission="android.permission.BIND_TV_INPUT" + android:process="com.android.tv.tuner.sample.network.tvinput" > + <intent-filter> + <action android:name="android.media.tv.TvInputService" /> + </intent-filter> + + <meta-data + android:name="android.media.tv.input" + android:resource="@xml/sample_network_tvinputservice" /> + </service> + <service + android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService" + android:exported="false" + android:permission="android.permission.BIND_JOB_SERVICE" + android:process="com.android.tv.tuner" /> + </application> + +</manifest>
\ No newline at end of file diff --git a/tuner/SampleNetworkTuner/build.gradle b/tuner/SampleNetworkTuner/build.gradle new file mode 100644 index 00000000..657a4258 --- /dev/null +++ b/tuner/SampleNetworkTuner/build.gradle @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +buildscript { + repositories { + mavenCentral() + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5' + } +} +apply plugin: 'com.android.application' +apply plugin: 'com.google.protobuf' +android { + compileSdkVersion 26 + buildToolsVersion '28.0.2' + + dexOptions { + preDexLibraries = false + additionalParameters=['--core-library'] + javaMaxHeapSize "6g" + } + android { + defaultConfig { + resConfigs "en" + } + } + defaultConfig { + minSdkVersion 23 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + minifyEnabled false + } + } + compileOptions() { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src', '../../partner_support/src'] + manifest.srcFile 'AndroidManifest.xml' + } + } +} + +repositories { + mavenCentral() + jcenter() + google() +} + +final String SUPPORT_LIBS_VERSION = '26.1.0' +dependencies { + implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0' + implementation 'com.google.android.exoplayer:exoplayer:r1.5.16' + implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}" + /*Not building with latest one (1.6.2)*/ + annotationProcessor 'com.google.auto.value:auto-value:1.5.4' + implementation 'com.google.auto.value:auto-value:1.5.4' + implementation project(':common') + implementation project(':tuner') +} diff --git a/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..e0d8e84a --- /dev/null +++ b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png diff --git a/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..5e99cdf5 --- /dev/null +++ b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png diff --git a/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..2f6336fb --- /dev/null +++ b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..d14775be --- /dev/null +++ b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..ab4e8dfd --- /dev/null +++ b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/tuner/SampleNetworkTuner/res/values/strings.xml b/tuner/SampleNetworkTuner/res/values/strings.xml new file mode 100644 index 00000000..47a4c284 --- /dev/null +++ b/tuner/SampleNetworkTuner/res/values/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Name of this application. It appears in TV app UI, as one of available TV inputs. --> + <string name="sample_network_tuner_app_name" translatable="false">Sample Network Tuner</string> +</resources>
\ No newline at end of file diff --git a/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml new file mode 100644 index 00000000..7e6083db --- /dev/null +++ b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright (C) 2015 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. + --> + +<!-- +/** + * Copyright (c) 2014, 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-input xmlns:android="http://schemas.android.com/apk/res/android" + android:setupActivity="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity" + android:canRecord="true" + android:tunerCount="1" /> diff --git a/tuner/SampleNetworkTuner/settings.gradle b/tuner/SampleNetworkTuner/settings.gradle new file mode 100644 index 00000000..13cb90f8 --- /dev/null +++ b/tuner/SampleNetworkTuner/settings.gradle @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +include ':common' +include ':tuner' +project(":common").projectDir = file("../../common") +project(":tuner").projectDir = file(".././") diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml new file mode 100644 index 00000000..dddd8a4b --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 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" + package="com.android.tv.tuner.sample.network" xmlns:tools="http://schemas.android.com/tools"> + + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <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" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> + <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> + + <!-- Permissions/feature for USB tuner --> + <uses-permission android:name="android.permission.DVB_DEVICE" /> + <uses-feature android:name="android.hardware.usb.host" android:required="false" /> + + <!-- Limit only for Android TV --> + <uses-feature android:name="android.software.leanback" android:required="true" /> + <uses-feature android:name="android.software.live_tv" android:required="true" /> + <uses-feature android:name="android.hardware.touchscreen" android:required="false"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> + <application + android:name=".app.SampleNetworkTuner" + android:icon="@mipmap/ic_launcher" + android:label="@string/sample_network_tuner_app_name" + /> +</manifest> diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md new file mode 100644 index 00000000..9df86c53 --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md @@ -0,0 +1,37 @@ +# SampleNetworkTuner + +SampleNetworkTuner is the reference DVB Tuner. Partners should copy these files +to their own directory and modify as needed. + +## Prerequisites + +* A DVB Tuner + * A Nexus player with a USB Tuner attached will work. +* system privileged app + * The DVB_DEVICE permission requires the app to be a privileged system app + +## First install + +#### Root + +```bash +adb root +adb remount +``` + +### modify privapp-permissions-atv.xml + +Edit system/etc/permissions/privapp-permissions-atv.xml + +```xml +<privapp-permissions package="com.android.tv.tuner.sample.network"> + <permission name="android.permission.DVB_DEVICE"/> +</privapp-permissions> +``` + +### Push to system/priv-app + +```bash +adb shell mkdir /system/priv-app/SampleNetworkTuner +adb push <path to apk> /system/priv-app/SampleNetworkTuner/SampleNetworkTuner.apk +``` diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java new file mode 100644 index 00000000..eb5b2ad4 --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.app; + +import android.content.ComponentName; +import android.media.tv.TvContract; +import com.android.tv.common.BaseApplication; +import com.android.tv.common.singletons.HasSingletons; +import com.android.tv.tuner.modules.TunerSingletonsModule; +import com.android.tv.tuner.sample.network.singletons.SampleNetworkSingletons; +import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl; +import dagger.android.AndroidInjector; +import com.android.tv.common.flags.CloudEpgFlags; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; +import javax.inject.Inject; + +/** The top level application for Sample DVB Tuner. */ +public class SampleNetworkTuner extends BaseApplication + implements SampleNetworkSingletons, HasSingletons<SampleNetworkSingletons> { + + private String mEmbeddedInputId; + @Inject CloudEpgFlags mCloudEpgFlags; + @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + @Inject TunerSessionFactoryImpl mTunerSessionFactory; + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + protected AndroidInjector<SampleNetworkTuner> applicationInjector() { + return DaggerSampleNetworkTunerComponent.builder() + .sampleNetworkTunerModule(new SampleNetworkTunerModule(this)) + .tunerSingletonsModule(new TunerSingletonsModule(this)) + .build(); + } + + @Override + public synchronized String getEmbeddedTunerInputId() { + if (mEmbeddedInputId == null) { + mEmbeddedInputId = + TvContract.buildInputId( + new ComponentName(this, SampleNetworkTunerTvInputService.class)); + } + return mEmbeddedInputId; + } + + @Override + public CloudEpgFlags getCloudEpgFlags() { + return mCloudEpgFlags; + } + + @Override + public BuildType getBuildType() { + return BuildType.ENG; + } + + @Override + public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() { + return mConcurrentDvrPlaybackFlags; + } + + @Override + public SampleNetworkSingletons singletons() { + return this; + } + + public TunerSessionFactory getTunerSessionFactory() { + return mTunerSessionFactory; + } +} diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java new file mode 100644 index 00000000..b10105b3 --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.app; + +import dagger.Component; +import dagger.android.AndroidInjectionModule; +import dagger.android.AndroidInjector; +import javax.inject.Singleton; + +/** Dagger component for {@link SampleNetworkTuner}. */ +@Singleton +@Component(modules = {AndroidInjectionModule.class, SampleNetworkTunerModule.class}) +public interface SampleNetworkTunerComponent extends AndroidInjector<SampleNetworkTuner> {} diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java new file mode 100644 index 00000000..d974e20a --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.app; + +import com.android.tv.common.flags.impl.DefaultFlagsModule; +import com.android.tv.tuner.api.TunerFactory; +import com.android.tv.tuner.builtin.BuiltInTunerHalFactory; +import com.android.tv.tuner.modules.TunerModule; +import com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity; +import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import dagger.Module; +import dagger.Provides; + +/** Dagger module for {@link SampleNetworkTuner}. */ +@Module( + includes = { + DefaultFlagsModule.class, + SampleNetworkTunerTvInputService.Module.class, + SampleNetworkTunerSetupActivity.Module.class, + TunerModule.class, + }) +class SampleNetworkTunerModule { + private final SampleNetworkTuner mSampleNetworkTuner; + + SampleNetworkTunerModule(SampleNetworkTuner sampleNetworkTuner) { + mSampleNetworkTuner = sampleNetworkTuner; + } + + @Provides + public TunerSessionFactory providesTunerSessionFactory() { + return mSampleNetworkTuner.getTunerSessionFactory(); + } + + @Provides + TunerFactory providesTunerFactory() { + return BuiltInTunerHalFactory.INSTANCE; + } +} diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java new file mode 100644 index 00000000..fd783c4f --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.setup; + +import android.app.FragmentManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.tv.TvInputInfo; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.singletons.HasSingletons; +import com.android.tv.common.ui.setup.SetupFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.common.util.PostalCodeUtils; +import com.android.tv.tuner.sample.network.R; +import com.android.tv.tuner.setup.BaseTunerSetupActivity; +import com.android.tv.tuner.setup.ConnectionTypeFragment; +import com.android.tv.tuner.setup.LineupFragment; +import com.android.tv.tuner.setup.LocationFragment; +import com.android.tv.tuner.setup.PostalCodeFragment; +import com.android.tv.tuner.setup.ScanFragment; +import com.android.tv.tuner.setup.ScanResultFragment; +import com.android.tv.tuner.setup.WelcomeFragment; +import com.android.tv.tuner.singletons.TunerSingletons; +import com.google.android.tv.partner.support.EpgContract; +import com.google.android.tv.partner.support.EpgInput; +import com.google.android.tv.partner.support.EpgInputs; +import com.google.android.tv.partner.support.Lineup; +import com.google.android.tv.partner.support.Lineups; +import com.google.android.tv.partner.support.TunerSetupUtils; +import dagger.android.ContributesAndroidInjector; +import java.util.ArrayList; +import java.util.List; + +/** An activity that serves Live TV tuner setup process. */ +public class SampleNetworkTunerSetupActivity extends BaseTunerSetupActivity { + private static final String TAG = "SampleNetworkTunerSetupActivity"; + private static final boolean DEBUG = false; + + private static final int FETCH_LINEUP_TIMEOUT_MS = 10000; // 10 seconds + private static final int FETCH_LINEUP_RETRY_TIMEOUT_MS = 20000; // 20 seconds + private static final String OTAD_PREFIX = "OTAD"; + private static final String STRING_BROADCAST_DIGITAL = "Broadcast Digital"; + + private LineupFragment currentLineupFragment; + + private List<String> channelNumbers; + private List<Lineup> lineups; + private Lineup selectedLineup; + private List<Pair<Lineup, Integer>> lineupMatchCountPair; + private FetchLineupTask fetchLineupTask; + private EpgInput epgInput; + private String postalCode; + private final Handler handler = new Handler(); + private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup; + private String embeddedInputId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (DEBUG) { + Log.d(TAG, "onCreate"); + } + embeddedInputId = + HasSingletons.get(TunerSingletons.class, getApplicationContext()) + .getEmbeddedTunerInputId(); + new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + protected void executeGetTunerTypeAndCountAsyncTask() { + new AsyncTask<Void, Void, Integer>() { + @Override + protected Integer doInBackground(Void... arg0) { + return mTunerFactory.getTunerTypeAndCount(SampleNetworkTunerSetupActivity.this) + .first; + } + + @Override + protected void onPostExecute(Integer result) { + if (!SampleNetworkTunerSetupActivity.this.isDestroyed()) { + mTunerType = result; + if (result == null) { + finish(); + } else if (!mActivityStopped) { + showInitialFragment(); + } else { + mPendingShowInitialFragment = true; + } + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + protected boolean executeAction(String category, int actionId, Bundle params) { + switch (category) { + case WelcomeFragment.ACTION_CATEGORY: + switch (actionId) { + case SetupMultiPaneFragment.ACTION_DONE: + super.executeAction(category, actionId, params); + break; + default: + String postalCode = PostalCodeUtils.getLastPostalCode(this); + boolean needLocation = + CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( + getApplicationContext()) + && TextUtils.isEmpty(postalCode); + if (needLocation + && checkSelfPermission( + android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + showLocationFragment(); + } else if (mNeedToShowPostalCodeFragment || needLocation) { + // We cannot get postal code automatically. Postal code input fragment + // should always be shown even if users have input some valid postal + // code in this activity before. + mNeedToShowPostalCodeFragment = true; + showPostalCodeFragment(); + } else { + lineups = null; + selectedLineup = null; + this.postalCode = postalCode; + restartFetchLineupTask(); + showConnectionTypeFragment(); + } + break; + } + return true; + case LocationFragment.ACTION_CATEGORY: + switch (actionId) { + case LocationFragment.ACTION_ALLOW_PERMISSION: + String postalCode = + params == null + ? null + : params.getString(LocationFragment.KEY_POSTAL_CODE); + if (postalCode == null) { + showPostalCodeFragment(); + } else { + this.postalCode = postalCode; + restartFetchLineupTask(); + showConnectionTypeFragment(); + } + break; + default: + cancelFetchLineup(); + showConnectionTypeFragment(); + } + return true; + case PostalCodeFragment.ACTION_CATEGORY: + lineups = null; + selectedLineup = null; + switch (actionId) { + case SetupMultiPaneFragment.ACTION_DONE: + String postalCode = params.getString(PostalCodeFragment.KEY_POSTAL_CODE); + if (postalCode != null) { + this.postalCode = postalCode; + restartFetchLineupTask(); + } + // fall through + case SetupMultiPaneFragment.ACTION_SKIP: + showConnectionTypeFragment(); + break; + default: // fall out + } + return true; + case ConnectionTypeFragment.ACTION_CATEGORY: + channelNumbers = null; + lineupMatchCountPair = null; + return super.executeAction(category, actionId, params); + case ScanFragment.ACTION_CATEGORY: + switch (actionId) { + case ScanFragment.ACTION_CANCEL: + getFragmentManager().popBackStack(); + return true; + case ScanFragment.ACTION_FINISH: + clearTunerHal(); + channelNumbers = + params.getStringArrayList(ScanFragment.KEY_CHANNEL_NUMBERS); + selectedLineup = null; + if (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( + getApplicationContext()) + && channelNumbers != null + && !channelNumbers.isEmpty() + && !TextUtils.isEmpty(this.postalCode)) { + showLineupFragment(); + } else { + showScanResultFragment(); + } + return true; + default: // fall out + } + break; + case LineupFragment.ACTION_CATEGORY: + switch (actionId) { + case LineupFragment.ACTION_SKIP: + selectedLineup = null; + currentLineupFragment = null; + showScanResultFragment(); + break; + case LineupFragment.ACTION_ID_RETRY: + currentLineupFragment.onRetry(); + restartFetchLineupTask(); + handler.postDelayed( + cancelFetchLineupTaskRunnable, FETCH_LINEUP_RETRY_TIMEOUT_MS); + break; + default: + if (actionId >= 0 && actionId < lineupMatchCountPair.size()) { + if (DEBUG) { + if (selectedLineup != null) { + Log.d( + TAG, + "Lineup " + selectedLineup.getName() + " is selected."); + } + } + selectedLineup = lineupMatchCountPair.get(actionId).first; + } + currentLineupFragment = null; + showScanResultFragment(); + break; + } + return true; + case ScanResultFragment.ACTION_CATEGORY: + switch (actionId) { + case SetupMultiPaneFragment.ACTION_DONE: + new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + break; + default: + // scan again + if (lineups == null || lineups.isEmpty()) { + lineups = null; + restartFetchLineupTask(); + } + super.executeAction(category, actionId, params); + break; + } + return true; + default: // fall out + } + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + FragmentManager manager = getFragmentManager(); + int count = manager.getBackStackEntryCount(); + if (count > 0) { + String lastTag = manager.getBackStackEntryAt(count - 1).getName(); + if (LineupFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { + // Pops fragment including ScanFragment. + manager.popBackStack( + manager.getBackStackEntryAt(count - 2).getName(), + FragmentManager.POP_BACK_STACK_INCLUSIVE); + return true; + } + if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { + String secondLastTag = manager.getBackStackEntryAt(count - 2).getName(); + if (ScanFragment.class.getCanonicalName().equals(secondLastTag)) { + // Pops fragment including ScanFragment. + manager.popBackStack( + secondLastTag, FragmentManager.POP_BACK_STACK_INCLUSIVE); + return true; + } + if (LineupFragment.class.getCanonicalName().equals(secondLastTag)) { + currentLineupFragment = + (LineupFragment) manager.findFragmentByTag(secondLastTag); + if (lineups == null || lineups.isEmpty()) { + lineups = null; + restartFetchLineupTask(); + } + } + } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) { + mLastScanFragment.finishScan(true); + return true; + } + } + } + return super.onKeyUp(keyCode, event); + } + + private void showLineupFragment() { + if (lineupMatchCountPair == null && lineups != null) { + lineupMatchCountPair = TunerSetupUtils.lineupChannelMatchCount(lineups, channelNumbers); + } + currentLineupFragment = new LineupFragment(); + currentLineupFragment.setArguments(getArgsForLineupFragment()); + currentLineupFragment.setShortDistance( + SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); + handler.removeCallbacksAndMessages(null); + showFragment(currentLineupFragment, true); + handler.postDelayed(cancelFetchLineupTaskRunnable, FETCH_LINEUP_TIMEOUT_MS); + } + + private Bundle getArgsForLineupFragment() { + Bundle args = new Bundle(); + if (lineupMatchCountPair == null) { + return args; + } + ArrayList<String> lineupNames = new ArrayList<>(lineupMatchCountPair.size()); + ArrayList<Integer> matchNumbers = new ArrayList<>(lineupMatchCountPair.size()); + int defaultLineupIndex = 0; + for (Pair<Lineup, Integer> pair : lineupMatchCountPair) { + Lineup lineup = pair.first; + String name; + if (!TextUtils.isEmpty(lineup.getName())) { + name = lineup.getName(); + } else { + name = lineup.getId(); + } + if (name.equals(OTAD_PREFIX + postalCode) || name.equals(STRING_BROADCAST_DIGITAL)) { + // rename OTA / antenna lineups + name = getString(R.string.ut_lineup_name_antenna); + } + lineupNames.add(name); + matchNumbers.add(pair.second); + if (epgInput != null && TextUtils.equals(lineup.getId(), epgInput.getLineupId())) { + // The last index is the current one. + defaultLineupIndex = lineupNames.size() - 1; + } + } + args.putStringArrayList(LineupFragment.KEY_LINEUP_NAMES, lineupNames); + args.putIntegerArrayList(LineupFragment.KEY_MATCH_NUMBERS, matchNumbers); + args.putInt(LineupFragment.KEY_DEFAULT_LINEUP, defaultLineupIndex); + return args; + } + + private void cancelFetchLineup() { + if (fetchLineupTask == null) { + return; + } + AsyncTask.Status status = fetchLineupTask.getStatus(); + if (status == AsyncTask.Status.RUNNING || status == AsyncTask.Status.PENDING) { + fetchLineupTask.cancel(true); + fetchLineupTask = null; + if (currentLineupFragment != null) { + currentLineupFragment.onLineupNotFound(); + } + } + } + + private void restartFetchLineupTask() { + if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext()) + || TextUtils.isEmpty(postalCode) + || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + return; + } + if (fetchLineupTask != null) { + fetchLineupTask.cancel(true); + } + handler.removeCallbacksAndMessages(null); + fetchLineupTask = new FetchLineupTask(getContentResolver(), postalCode); + fetchLineupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private class FetchLineupTask extends AsyncTask<Void, Void, List<Lineup>> { + private final ContentResolver contentResolver; + private final String postalCode; + + FetchLineupTask(ContentResolver contentResolver, String postalCode) { + this.contentResolver = contentResolver; + this.postalCode = postalCode; + } + + @Override + protected List<Lineup> doInBackground(Void... args) { + if (contentResolver == null || TextUtils.isEmpty(postalCode)) { + return new ArrayList<>(); + } + return new ArrayList<>(Lineups.query(contentResolver, postalCode)); + } + + @Override + protected void onPostExecute(List<Lineup> lineups) { + if (DEBUG) { + if (lineups != null) { + Log.d(TAG, "FetchLineupTask fetched " + lineups.size() + " lineups"); + } else { + Log.d(TAG, "FetchLineupTask returned null"); + } + } + SampleNetworkTunerSetupActivity.this.lineups = lineups; + if (currentLineupFragment != null) { + if (lineups == null || lineups.isEmpty()) { + currentLineupFragment.onLineupNotFound(); + } else { + lineupMatchCountPair = + TunerSetupUtils.lineupChannelMatchCount( + SampleNetworkTunerSetupActivity.this.lineups, channelNumbers); + currentLineupFragment.onLineupFound(getArgsForLineupFragment()); + } + } + } + } + + private class InsertOrModifyEpgInputTask extends AsyncTask<Void, Void, Void> { + private final Lineup lineup; + private final String inputId; + + InsertOrModifyEpgInputTask(@Nullable Lineup lineup, String inputId) { + this.lineup = lineup; + this.inputId = inputId; + } + + @Override + protected Void doInBackground(Void... args) { + if (lineup == null + || (SampleNetworkTunerSetupActivity.this.epgInput != null + && TextUtils.equals( + lineup.getId(), + SampleNetworkTunerSetupActivity.this.epgInput.getLineupId()))) { + return null; + } + ContentValues values = new ContentValues(); + values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, inputId); + values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId()); + + ContentResolver contentResolver = getContentResolver(); + if (SampleNetworkTunerSetupActivity.this.epgInput != null) { + values.put( + EpgContract.EpgInputs.COLUMN_ID, + SampleNetworkTunerSetupActivity.this.epgInput.getId()); + EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values)); + return null; + } + EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, inputId); + if (epgInput == null) { + contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values); + } else { + values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId()); + EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values)); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + Intent data = new Intent(); + data.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId); + data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true); + setResult(RESULT_OK, data); + finish(); + } + } + + /** + * Exports {@link SampleNetworkTunerSetupActivity} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract SampleNetworkTunerSetupActivity + contributeSampleNetworkTunerSetupActivityInjector(); + } + + private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> { + private final String inputId; + + QueryEpgInputTask(String inputId) { + this.inputId = inputId; + } + + @Override + protected EpgInput doInBackground(Void... args) { + ContentResolver contentResolver = getContentResolver(); + return EpgInputs.queryEpgInput(contentResolver, inputId); + } + + @Override + protected void onPostExecute(EpgInput result) { + epgInput = result; + } + } +} diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java new file mode 100644 index 00000000..00a6e27a --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.singletons; + +import com.android.tv.common.BaseSingletons; +import com.android.tv.tuner.singletons.TunerSingletons; + +/** Singletons for SampleNetworkTuner. */ +public interface SampleNetworkSingletons extends BaseSingletons, TunerSingletons {} diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java new file mode 100644 index 00000000..de5ff229 --- /dev/null +++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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.tuner.sample.network.tvinput; + +import com.android.tv.tuner.tvinput.BaseTunerTvInputService; +import dagger.android.ContributesAndroidInjector; + +/** Sample DVB Tuner {@link android.media.tv.TvInputService}. */ +public class SampleNetworkTunerTvInputService extends BaseTunerTvInputService { + + /** + * Exports {@link SampleNetworkTunerTvInputService} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract SampleNetworkTunerTvInputService + contributesSampleNetworkTunerTvInputServiceInjector(); + } +} diff --git a/tuner/build.gradle b/tuner/build.gradle new file mode 100644 index 00000000..0f40a29b --- /dev/null +++ b/tuner/build.gradle @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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. + */ + + +/* + * Experimental gradle configuration. This file may not be up to date. + */ + +apply plugin: 'com.android.library' +apply plugin: 'com.google.protobuf' +buildscript { + repositories { + mavenCentral() + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5' + } +} +android { + compileSdkVersion 26 + buildToolsVersion '28.0.2' + + dexOptions { + preDexLibraries = false + additionalParameters = ['--core-library'] + javaMaxHeapSize "6g" + } + + android { + defaultConfig { + resConfigs "en" + } + } + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + minifyEnabled false + } + release { + minifyEnabled true + } + } + compileOptions() { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + res.srcDirs = ['res'] + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + proto { + srcDir 'proto/' + } + } + } +} + +repositories { + mavenCentral() + google() + jcenter() +} + +final String SUPPORT_LIBS_VERSION = '26.1.0' +dependencies { + implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0' + implementation 'com.google.android.exoplayer:exoplayer:r1.5.16' + implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:appcompat-v7:${SUPPORT_LIBS_VERSION}" + implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}" + implementation 'com.google.guava:guava:26.0-android' + implementation 'com.google.protobuf.nano:protobuf-javanano:3.2.0rc2' + implementation project(':common') +} +protobuf { + // Configure the protoc executable + protoc { + artifact = 'com.google.protobuf:protoc:3.1.0' + + generateProtoTasks { + all().each { + task -> task.builtins { + remove java + javanano { + option "enum_style=java" + } + } + } + } + } +}
\ No newline at end of file diff --git a/tuner/buildconfig.mk b/tuner/buildconfig.mk deleted file mode 100644 index cece7f2b..00000000 --- a/tuner/buildconfig.mk +++ /dev/null @@ -1,39 +0,0 @@ -# -# Copyright (C) 2015 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. -# - -# Emulate gradles BuildConfig.java - -ifeq "$(TARGET_BUILD_VARIANT)" "eng" - BC_DEBUG_STATUS := true -else ifeq "$(TARGET_BUILD_VARIANT)" "userdebug" - BC_DEBUG_STATUS := true -else - BC_DEBUG_STATUS := false -endif - -ifeq "$(TARGET_BUILD_VARIANT)" "eng" - BC_ENG_STATUS := true -else - BC_ENG_STATUS := false -endif - -gen := $(local-generated-sources-dir)/$(TARGET_BUILD_VARIANT)/BuildConfig.java -$(gen): PRIVATE_CUSTOM_TOOL = sed -e \ - 's/%DEBUG%/$(BC_DEBUG_STATUS)/;s/%ENG%/$(BC_ENG_STATUS)/' \ - $< > $@ -$(gen) : $(LOCAL_PATH)/BuildConfig.java.in - $(transform-generated-source) -LOCAL_GENERATED_SOURCES += $(gen)
\ No newline at end of file diff --git a/tuner/proto/Android.bp b/tuner/proto/Android.bp new file mode 100644 index 00000000..67f35f82 --- /dev/null +++ b/tuner/proto/Android.bp @@ -0,0 +1,27 @@ +// +// Copyright (C) 2019 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. +// + +java_library { + name: "live-tv-tuner-proto", + srcs: ["*.proto"], + sdk_version: "system_current", + proto: { + type: "nano", + output_params: ["enum_style=java"], + canonical_path_from_root: false, + }, + min_sdk_version: "23", +} diff --git a/tuner/proto/channel.proto b/tuner/proto/channel.proto index 1f994522..ff372ad5 100644 --- a/tuner/proto/channel.proto +++ b/tuner/proto/channel.proto @@ -21,10 +21,15 @@ package com.android.tv.tuner.data; option java_package = "com.android.tv.tuner.data"; option java_outer_classname = "Channel"; + +// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto"; + import "track.proto"; // Holds information about a channel used in the tuners. message TunerChannelProto { +// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false; + optional TunerType type = 1; optional string short_name = 2; optional string long_name = 3; @@ -60,6 +65,8 @@ message TunerChannelProto { // Enum describing the types of tuner. enum TunerType { +// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false; + TYPE_TUNER = 0; TYPE_FILE = 1; TYPE_NETWORK = 2; @@ -67,6 +74,8 @@ enum TunerType { // Enum describing the types of video stream. enum VideoStreamType { +// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false; + // ISO/IEC 11172 Video (MPEG-1) MPEG1 = 0x01; // ISO/IEC 13818-2 (MPEG-2) Video @@ -81,6 +90,8 @@ enum VideoStreamType { // Enum describing the types of audio stream. enum AudioStreamType { +// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false; + // ISO/IEC 11172 Audio (MPEG-1) MPEG1AUDIO = 0x03; // ISO/IEC 13818-3 Audio (MPEG-2) @@ -98,6 +109,8 @@ enum AudioStreamType { // Enum describing ATSC service types // See ATSC Code Points Registry. enum AtscServiceType { +// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false; + SERVICE_TYPE_ATSC_RESERVED = 0x0; SERVICE_TYPE_ANALOG_TELEVISION_CHANNELS = 0x1; SERVICE_TYPE_ATSC_DIGITAL_TELEVISION = 0x2; diff --git a/tuner/proto/track.proto b/tuner/proto/track.proto index fe60fed5..11ca784d 100644 --- a/tuner/proto/track.proto +++ b/tuner/proto/track.proto @@ -18,11 +18,15 @@ syntax = "proto2"; package com.android.tv.tuner.data; +// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto"; + option java_package = "com.android.tv.tuner.data"; option java_outer_classname = "Track"; // Represents a AC3 audio track. message AtscAudioTrack { +// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false; + optional string language = 1; optional AudioType audio_type = 2; optional int32 index = 3; @@ -32,6 +36,8 @@ message AtscAudioTrack { // Enum describing the types of a audio track. // See ISO/IEC 138181-1:2000(e) Table 2-53. enum AudioType { +// AOSP_Comment_Out option (proto2.nano.enum_as_lite) = false; + AUDIOTYPE_UNDEFINED = 0; AUDIOTYPE_CLEAN_EFFECTS = 1; AUDIOTYPE_HEARING_IMPAIRED = 2; @@ -41,6 +47,8 @@ message AtscAudioTrack { // Represents a CEA-708 caption track. message AtscCaptionTrack { +// AOSP_Comment_Out option (proto2.nano.message_as_lite) = false; + optional string language = 1; optional int32 service_number = 2; optional bool easy_reader = 3; diff --git a/tuner/res/values/strings.xml b/tuner/res/values/strings.xml index 58d7214c..96aca8a2 100644 --- a/tuner/res/values/strings.xml +++ b/tuner/res/values/strings.xml @@ -210,10 +210,40 @@ <!-- Title of postal/zip code input guided step fragment [CHAR LIMIT=30] --> <string name="postal_code_guidance_title">Enter your ZIP Code.</string> <!-- Description of postal/zip code input guided step fragment [CHAR LIMIT=NONE] --> - <string name="postal_code_guidance_description">Live TV app will use the ZIP Code to provide a complete program guide for the TV channels.</string> + <string name="postal_code_guidance_description"> + Please enter your ZIP code. + <xlgiff id="break">\n</xlgiff> + Your ZIP code is stored on device locally and sent to Google servers when updating your + program information. The ZIP code sent is never associated with your account or stored in + Google servers. + <xlgiff id="break">\n</xlgiff> + If you do not provide your ZIP code, your TV program guide will be limited.</string> + <!-- Description prefix of postal/zip code input guided step fragment when failed to get location automatically. [CHAR LIMIT=NONE] --> + <string name="postal_code_guidance_description_get_location_failed"> + <xliff:g id="app_name">Live TV</xliff:g> cannot determine your location.</string> <!-- Description of postal/zip code input edit text view to prompt users entering ZIP Code [CHAR LIMIT=30] --> <string name="postal_code_action_description">Enter your ZIP Code</string> <!-- Warning message shown in description field of postal/zip code input edit text view when user enters an invalid ZIP Code and presses Done [CHAR LIMIT=30] --> <string name="postal_code_invalid_warning">Invalid ZIP Code</string> + <!-- Title of location rationale guided step fragment [CHAR LIMIT=30] --> + <string name="location_guidance_title">Location</string> + <!-- Description of location rationale guided step fragment [CHAR LIMIT=NONE] --> + <string name="location_guidance_description"> + <xliff:g id="app_name">Live TV</xliff:g> uses your ZIP code to provide a complete + program guide for your TV channels. + <xlgiff id="break">\n</xlgiff> + Your ZIP code is stored on device locally and sent to Google servers when updating your + program information. The ZIP code sent is never associated with your account or stored in + Google servers. + <xlgiff id="break">\n</xlgiff> + If you do not grant permission or do not manually enter your ZIP code, your TV program guide + will be limited.</string> + <!-- Grant location permission --> + <string name="location_choices_allow_permission">Grant Permission</string> + <!-- Enter postal code --> + <string name="location_choices_enter_zip_code">Enter Zip Code</string> + <!-- Message to show users when getting location information --> + <string name="location_choices_getting_location">Getting Location</string> + </resources> diff --git a/tuner/src/com/android/tv/tuner/DvbTunerHal.java b/tuner/src/com/android/tv/tuner/DvbTunerHal.java index 4375fc32..c802ebbb 100644 --- a/tuner/src/com/android/tv/tuner/DvbTunerHal.java +++ b/tuner/src/com/android/tv/tuner/DvbTunerHal.java @@ -19,6 +19,7 @@ package com.android.tv.tuner; import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; +import com.android.tv.common.compat.TvInputConstantCompat; import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper; import java.util.List; import java.util.SortedSet; @@ -26,13 +27,18 @@ import java.util.TreeSet; /** A class to handle a hardware Linux DVB API supported tuner device. */ public class DvbTunerHal extends TunerHal { + private static final String TAG = "DvbTunerHal"; + private static final boolean DEBUG = false; private static final Object sLock = new Object(); // @GuardedBy("sLock") private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>(); + // The minimum delta for updating signal strength when valid + private static final int SIGNAL_STRENGTH_MINIMUM_DELTA = 2; private final DvbDeviceAccessor mDvbDeviceAccessor; private DvbDeviceInfoWrapper mDvbDeviceInfo; + private int mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; public DvbTunerHal(Context context) { super(context); @@ -40,7 +46,7 @@ public class DvbTunerHal extends TunerHal { } @Override - protected boolean openFirstAvailable() { + public boolean openFirstAvailable() { List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList(); if (deviceInfoList == null || deviceInfoList.isEmpty()) { Log.e(TAG, "There's no dvb device attached"); @@ -115,12 +121,12 @@ public class DvbTunerHal extends TunerHal { } @Override - protected boolean isDeviceOpen() { + public boolean isDeviceOpen() { return (mDvbDeviceInfo != null); } @Override - protected long getDeviceId() { + public long getDeviceId() { if (mDvbDeviceInfo != null) { return mDvbDeviceInfo.getId(); } @@ -174,4 +180,45 @@ public class DvbTunerHal extends TunerHal { return 0; } } + + @Override + public int getSignalStrength() { + int signalStrength; + signalStrength = nativeGetSignalStrength(getDeviceId()); + if (signalStrength == -3) { + mSignalStrength = signalStrength; + return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; + } + if (signalStrength > 65535 || signalStrength < 0) { + mSignalStrength = signalStrength; + return TvInputConstantCompat.SIGNAL_STRENGTH_ERROR; + } + signalStrength = getCurvedSignalStrength(signalStrength); + return updatingSignal(signalStrength); + } + + /** + * This method curves the raw signal strength from tuner when it's between 0 - 65535 inclusive. + */ + private int getCurvedSignalStrength(int signalStrength) { + /** When value < 80% of 65535, it will be recognized as level 0. */ + if (signalStrength < 65535 * 0.8) { + return 0; + } + /** When value is between 80% to 100% of 65535, it will be linearly mapped to 0 - 100%. */ + return (int) (5 * (signalStrength * 100.0 / 65535) - 400); + } + + /** + * This method is for noise canceling. If the delta between current and previous strength is + * less than {@link #SIGNAL_STRENGTH_MINIMUM_DELTA}, previous signal strength will be returned. + * Otherwise current signal strength will be updated and returned. + */ + private int updatingSignal(int signal) { + int delta = Math.abs(signal - mSignalStrength); + if (delta > SIGNAL_STRENGTH_MINIMUM_DELTA) { + mSignalStrength = signal; + } + return mSignalStrength; + } } diff --git a/tuner/src/com/android/tv/tuner/TunerFeatures.java b/tuner/src/com/android/tv/tuner/TunerFeatures.java deleted file mode 100644 index e682e636..00000000 --- a/tuner/src/com/android/tv/tuner/TunerFeatures.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2017 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.tuner; - -import static com.android.tv.common.feature.FeatureUtils.OFF; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import com.android.tv.common.BaseApplication; -import com.android.tv.common.config.api.RemoteConfig; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.common.feature.Feature; -import com.android.tv.common.feature.Model; -import com.android.tv.common.feature.PropertyFeature; -import com.android.tv.common.util.CommonUtils; -import com.android.tv.common.util.LocationUtils; -import java.util.Locale; - -/** - * List of {@link Feature} for Tuner. - * - * <p>Remove the {@code Feature} once it is launched. - */ -public class TunerFeatures extends CommonFeatures { - private static final String TAG = "TunerFeatures"; - private static final boolean DEBUG = false; - - /** Use network tuner if it is available and there is no other tuner types. */ - public static final Feature NETWORK_TUNER = - new Feature() { - @Override - public boolean isEnabled(Context context) { - if (!TUNER.isEnabled(context)) { - return false; - } - if (CommonUtils.isDeveloper()) { - // Network tuner will be enabled for developers. - return true; - } - return Locale.US - .getCountry() - .equalsIgnoreCase(LocationUtils.getCurrentCountry(context)); - } - }; - - /** - * USE_SW_CODEC_FOR_SD - * - * <p>Prefer software based codec for SD channels. - */ - public static final Feature USE_SW_CODEC_FOR_SD = - PropertyFeature.create( - "use_sw_codec_for_sd", - false - ); - - /** Use AC3 software decode. */ - public static final Feature AC3_SOFTWARE_DECODE = - new Feature() { - private final String[] SUPPORTED_REGIONS = {}; - - private Boolean mEnabled; - - @Override - public boolean isEnabled(Context context) { - if (mEnabled == null) { - if (mEnabled == null) { - // We will not cache the result of fallback solution. - String country = LocationUtils.getCurrentCountry(context); - for (int i = 0; i < SUPPORTED_REGIONS.length; ++i) { - if (SUPPORTED_REGIONS[i].equalsIgnoreCase(country)) { - return true; - } - } - if (DEBUG) Log.d(TAG, "AC3 flag false after country check"); - return false; - } - } - if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled); - return mEnabled; - } - }; - - /** Enable Dvb parsers and listeners. */ - public static final Feature ENABLE_FILE_DVB = OFF; - - private TunerFeatures() {} -} diff --git a/tuner/src/com/android/tv/tuner/TunerHal.java b/tuner/src/com/android/tv/tuner/TunerHal.java index 5801406b..dce4f4c4 100644 --- a/tuner/src/com/android/tv/tuner/TunerHal.java +++ b/tuner/src/com/android/tv/tuner/TunerHal.java @@ -17,87 +17,25 @@ package com.android.tv.tuner; import android.content.Context; -import android.support.annotation.IntDef; -import android.support.annotation.StringDef; -import android.support.annotation.WorkerThread; import android.util.Log; -import android.util.Pair; import com.android.tv.common.BuildConfig; -import com.android.tv.common.customization.CustomizationManager; - - +import com.android.tv.common.compat.TvInputConstantCompat; +import com.android.tv.tuner.api.Tuner; import com.android.tv.common.annotation.UsedByNative; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** A base class to handle a hardware tuner device. */ -public abstract class TunerHal implements AutoCloseable { - protected static final String TAG = "TunerHal"; - protected static final boolean DEBUG = false; - - @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR}) - @Retention(RetentionPolicy.SOURCE) - public @interface FilterType {} - - public static final int FILTER_TYPE_OTHER = 0; - public static final int FILTER_TYPE_AUDIO = 1; - public static final int FILTER_TYPE_VIDEO = 2; - public static final int FILTER_TYPE_PCR = 3; - - @StringDef({MODULATION_8VSB, MODULATION_QAM256}) - @Retention(RetentionPolicy.SOURCE) - public @interface ModulationType {} - - public static final String MODULATION_8VSB = "8VSB"; - public static final String MODULATION_QAM256 = "QAM256"; - - @IntDef({ - DELIVERY_SYSTEM_UNDEFINED, - DELIVERY_SYSTEM_ATSC, - DELIVERY_SYSTEM_DVBC, - DELIVERY_SYSTEM_DVBS, - DELIVERY_SYSTEM_DVBS2, - DELIVERY_SYSTEM_DVBT, - DELIVERY_SYSTEM_DVBT2 - }) - @Retention(RetentionPolicy.SOURCE) - public @interface DeliverySystemType {} +public abstract class TunerHal implements Tuner { + private static final String TAG = "TunerHal"; - public static final int DELIVERY_SYSTEM_UNDEFINED = 0; - public static final int DELIVERY_SYSTEM_ATSC = 1; - public static final int DELIVERY_SYSTEM_DVBC = 2; - public static final int DELIVERY_SYSTEM_DVBS = 3; - public static final int DELIVERY_SYSTEM_DVBS2 = 4; - public static final int DELIVERY_SYSTEM_DVBT = 5; - public static final int DELIVERY_SYSTEM_DVBT2 = 6; + private static final int PID_PAT = 0; + private static final int PID_ATSC_SI_BASE = 0x1ffb; + private static final int PID_DVB_SDT = 0x0011; + private static final int PID_DVB_EIT = 0x0012; + private static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000; + private static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for - @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK}) - @Retention(RetentionPolicy.SOURCE) - public @interface TunerType {} - - public static final int TUNER_TYPE_BUILT_IN = 1; - public static final int TUNER_TYPE_USB = 2; - public static final int TUNER_TYPE_NETWORK = 3; - - protected static final int PID_PAT = 0; - protected static final int PID_ATSC_SI_BASE = 0x1ffb; - protected static final int PID_DVB_SDT = 0x0011; - protected static final int PID_DVB_EIT = 0x0012; - protected static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000; - protected static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for - // QAM256 tuning. - @IntDef({ - BUILT_IN_TUNER_TYPE_LINUX_DVB - }) - @Retention(RetentionPolicy.SOURCE) - private @interface BuiltInTunerType {} - - private static final int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1; - - private static Integer sBuiltInTunerType; - - protected @DeliverySystemType int mDeliverySystemType; + @DeliverySystemType private int mDeliverySystemType; private boolean mIsStreaming; private int mFrequency; private String mModulation; @@ -108,66 +46,6 @@ public abstract class TunerHal implements AutoCloseable { } } - /** - * Creates a TunerHal instance. - * - * @param context context for creating the TunerHal instance - * @return the TunerHal instance - */ - @WorkerThread - public static synchronized TunerHal createInstance(Context context) { - TunerHal tunerHal = null; - if (DvbTunerHal.getNumberOfDevices(context) > 0) { - if (DEBUG) Log.d(TAG, "Use DvbTunerHal"); - tunerHal = new DvbTunerHal(context); - } - return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null; - } - - /** Gets the number of tuner devices currently present. */ - @WorkerThread - public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) { - if (useBuiltInTuner(context)) { - if (getBuiltInTunerType(context) == BUILT_IN_TUNER_TYPE_LINUX_DVB) { - return new Pair<>(TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context)); - } - } else { - int usbTunerCount = DvbTunerHal.getNumberOfDevices(context); - if (usbTunerCount > 0) { - return new Pair<>(TUNER_TYPE_USB, usbTunerCount); - } - } - return new Pair<>(null, 0); - } - - /** Check a delivery system is for DVB or not. */ - public static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) { - return deliverySystemType == DELIVERY_SYSTEM_DVBC - || deliverySystemType == DELIVERY_SYSTEM_DVBS - || deliverySystemType == DELIVERY_SYSTEM_DVBS2 - || deliverySystemType == DELIVERY_SYSTEM_DVBT - || deliverySystemType == DELIVERY_SYSTEM_DVBT2; - } - - /** - * Returns if tuner input service would use built-in tuners instead of USB tuners or network - * tuners. - */ - public static boolean useBuiltInTuner(Context context) { - return getBuiltInTunerType(context) != 0; - } - - private static @BuiltInTunerType int getBuiltInTunerType(Context context) { - if (sBuiltInTunerType == null) { - sBuiltInTunerType = 0; - if (CustomizationManager.hasLinuxDvbBuiltInTuner(context) - && DvbTunerHal.getNumberOfDevices(context) > 0) { - sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB; - } - } - return sBuiltInTunerType; - } - protected TunerHal(Context context) { mIsStreaming = false; mFrequency = -1; @@ -188,6 +66,7 @@ public abstract class TunerHal implements AutoCloseable { * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels of * the same frequency. */ + @Override public boolean isReusable() { return true; } @@ -201,18 +80,6 @@ public abstract class TunerHal implements AutoCloseable { protected native void nativeFinalize(long deviceId); /** - * Acquires the first available tuner device. If there is a tuner device that is available, the - * tuner device will be locked to the current instance. - * - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - protected abstract boolean openFirstAvailable(); - - protected abstract boolean isDeviceOpen(); - - protected abstract long getDeviceId(); - - /** * Sets the tuner channel. This should be called after acquiring a tuner device. * * @param frequency a frequency of the channel to tune to @@ -221,6 +88,7 @@ public abstract class TunerHal implements AutoCloseable { * use channelNumber instead of frequency for tune. * @return {@code true} if the operation was successful, {@code false} otherwise */ + @Override public synchronized boolean tune( int frequency, @ModulationType String modulation, String channelNumber) { if (!isDeviceOpen()) { @@ -237,7 +105,7 @@ public abstract class TunerHal implements AutoCloseable { if (mFrequency == frequency && Objects.equals(mModulation, modulation)) { addPidFilter(PID_PAT, FILTER_TYPE_OTHER); addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { + if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) { addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); } @@ -251,7 +119,7 @@ public abstract class TunerHal implements AutoCloseable { if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) { addPidFilter(PID_PAT, FILTER_TYPE_OTHER); addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { + if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) { addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); } @@ -273,6 +141,7 @@ public abstract class TunerHal implements AutoCloseable { * @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX) * @return {@code true} if the operation was successful, {@code false} otherwise */ + @Override public synchronized boolean addPidFilter(int pid, @FilterType int filterType) { if (!isDeviceOpen()) { Log.e(TAG, "There's no available device"); @@ -293,10 +162,13 @@ public abstract class TunerHal implements AutoCloseable { protected native int nativeGetDeliverySystemType(long deviceId); + protected native int nativeGetSignalStrength(long deviceId); + /** * Stops current tuning. The tuner device and pid filters will be reset by this call and make * the tuner ready to accept another tune request. */ + @Override public synchronized void stopTune() { if (isDeviceOpen()) { if (mIsStreaming) { @@ -309,10 +181,12 @@ public abstract class TunerHal implements AutoCloseable { mModulation = null; } + @Override public void setHasPendingTune(boolean hasPendingTune) { nativeSetHasPendingTune(getDeviceId(), hasPendingTune); } + @Override public int getDeliverySystemType() { return mDeliverySystemType; } @@ -320,9 +194,9 @@ public abstract class TunerHal implements AutoCloseable { protected native void nativeStopTune(long deviceId); /** - * This method must be called after {@link TunerHal#tune} and before {@link TunerHal#stopTune}. - * Writes at most maxSize TS frames in a buffer provided by the user. The frames employ MPEG - * encoding. + * This method must be called after {@link #tune(int, String, String)} and before {@link + * #stopTune()}. Writes at most maxSize TS frames in a buffer provided by the user. The frames + * employ MPEG encoding. * * @param javaBuffer a buffer to write the video data in * @param javaBufferSize the max amount of bytes to write in this buffer. Usually this number @@ -330,6 +204,7 @@ public abstract class TunerHal implements AutoCloseable { * @return the amount of bytes written in the buffer. Note that this value could be 0 if no new * frames have been obtained since the last call. */ + @Override public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) { if (isDeviceOpen()) { return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize); @@ -338,6 +213,21 @@ public abstract class TunerHal implements AutoCloseable { } } + /** + * This method gets signal strength for currently tuned channel. + * Each specific tuner should implement its own method. + * + * @return {@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED + * when signal check is not supported from tuner. + * {@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR} + * when signal returned is not valid. + * 0 - 100 representing strength from low to high. Curve raw data if necessary. + */ + @Override + public int getSignalStrength() { + return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; + } + protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize); /** diff --git a/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java new file mode 100644 index 00000000..e0319a27 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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.tuner.api; + +import com.android.tv.tuner.data.TunerChannel; + +/** Listener for detecting TV channels. */ +public interface ChannelScanListener { + + /** + * Fired when new information of an TV channel arrives. + * + * @param channel an TV channel + * @param channelArrivedAtFirstTime tells whether this channel arrived at first time + */ + void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime); +} diff --git a/tuner/src/com/android/tv/tuner/api/ScanChannel.java b/tuner/src/com/android/tv/tuner/api/ScanChannel.java new file mode 100644 index 00000000..56e5493c --- /dev/null +++ b/tuner/src/com/android/tv/tuner/api/ScanChannel.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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.tuner.api; + +import com.android.tv.tuner.data.nano.Channel; + +/** Channel information gathered from a <em>scan</em> */ +public final class ScanChannel { + public final int type; + public final int frequency; + public final String modulation; + public final String filename; + /** + * Radio frequency (channel) number specified at + * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code null} + * for cases like cable signal. + */ + public final Integer radioFrequencyNumber; + + public static ScanChannel forTuner( + int frequency, String modulation, Integer radioFrequencyNumber) { + return new ScanChannel( + Channel.TunerType.TYPE_TUNER, frequency, modulation, null, radioFrequencyNumber); + } + + public static ScanChannel forFile(int frequency, String filename) { + return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null); + } + + private ScanChannel( + int type, + int frequency, + String modulation, + String filename, + Integer radioFrequencyNumber) { + this.type = type; + this.frequency = frequency; + this.modulation = modulation; + this.filename = filename; + this.radioFrequencyNumber = radioFrequencyNumber; + } +} diff --git a/tuner/src/com/android/tv/tuner/api/Tuner.java b/tuner/src/com/android/tv/tuner/api/Tuner.java new file mode 100644 index 00000000..6f7e9d94 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/api/Tuner.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.tuner.api; + +import android.support.annotation.IntDef; +import android.support.annotation.StringDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** A interface a hardware tuner device. */ +public interface Tuner extends AutoCloseable { + + int FILTER_TYPE_OTHER = 0; + int FILTER_TYPE_AUDIO = 1; + int FILTER_TYPE_VIDEO = 2; + int FILTER_TYPE_PCR = 3; + String MODULATION_8VSB = "8VSB"; + String MODULATION_QAM256 = "QAM256"; + int DELIVERY_SYSTEM_UNDEFINED = 0; + int DELIVERY_SYSTEM_ATSC = 1; + int DELIVERY_SYSTEM_DVBC = 2; + int DELIVERY_SYSTEM_DVBS = 3; + int DELIVERY_SYSTEM_DVBS2 = 4; + int DELIVERY_SYSTEM_DVBT = 5; + int DELIVERY_SYSTEM_DVBT2 = 6; + int TUNER_TYPE_BUILT_IN = 1; + int TUNER_TYPE_USB = 2; + int TUNER_TYPE_NETWORK = 3; + int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1; + + /** Check a delivery system is for DVB or not. */ + static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) { + return deliverySystemType == DELIVERY_SYSTEM_DVBC + || deliverySystemType == DELIVERY_SYSTEM_DVBS + || deliverySystemType == DELIVERY_SYSTEM_DVBS2 + || deliverySystemType == DELIVERY_SYSTEM_DVBT + || deliverySystemType == DELIVERY_SYSTEM_DVBT2; + } + + boolean isReusable(); + + /** + * Acquires the first available tuner device. If there is a tuner device that is available, the + * tuner device will be locked to the current instance. + * + * @return {@code true} if the operation was successful, {@code false} otherwise + */ + boolean openFirstAvailable(); + + boolean isDeviceOpen(); + + long getDeviceId(); + + boolean tune(int frequency, @ModulationType String modulation, String channelNumber); + + boolean addPidFilter(int pid, @FilterType int filterType); + + void stopTune(); + + void setHasPendingTune(boolean hasPendingTune); + + int getDeliverySystemType(); + + int readTsStream(byte[] javaBuffer, int javaBufferSize); + + int getSignalStrength(); + + /** Filter type */ + @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR}) + @Retention(RetentionPolicy.SOURCE) + public @interface FilterType {} + + /** Modulation Type */ + @StringDef({MODULATION_8VSB, MODULATION_QAM256}) + @Retention(RetentionPolicy.SOURCE) + public @interface ModulationType {} + + /** Delivery System Type */ + @IntDef({ + DELIVERY_SYSTEM_UNDEFINED, + DELIVERY_SYSTEM_ATSC, + DELIVERY_SYSTEM_DVBC, + DELIVERY_SYSTEM_DVBS, + DELIVERY_SYSTEM_DVBS2, + DELIVERY_SYSTEM_DVBT, + DELIVERY_SYSTEM_DVBT2 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DeliverySystemType {} + + /** Tuner Type */ + @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK}) + @Retention(RetentionPolicy.SOURCE) + public @interface TunerType {} + + /** Built in tuner type */ + @IntDef({ + BUILT_IN_TUNER_TYPE_LINUX_DVB + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BuiltInTunerType {} +} diff --git a/tuner/src/com/android/tv/tuner/api/TunerFactory.java b/tuner/src/com/android/tv/tuner/api/TunerFactory.java new file mode 100644 index 00000000..bc29c7c9 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/api/TunerFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.tuner.api; + +import android.content.Context; +import android.support.annotation.WorkerThread; +import android.util.Pair; + +/** Factory for {@link Tuner}. */ +public interface TunerFactory { + @WorkerThread + Tuner createInstance(Context context); + + boolean useBuiltInTuner(Context context); + + @WorkerThread + Pair<Integer, Integer> getTunerTypeAndCount(Context context); +} diff --git a/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java new file mode 100644 index 00000000..9a0be740 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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.tuner.builtin; + +import android.content.Context; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.util.Pair; +import com.android.tv.common.customization.CustomizationManager; +import com.android.tv.common.feature.Model; +import com.android.tv.tuner.DvbTunerHal; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.api.TunerFactory; + + +/** TunerHal factory that creates all built in tuner types. */ +public final class BuiltInTunerHalFactory implements TunerFactory { + private static final String TAG = "BuiltInTunerHalFactory"; + private static final boolean DEBUG = false; + + private Integer mBuiltInTunerType; + + public static final TunerFactory INSTANCE = new BuiltInTunerHalFactory(); + + private BuiltInTunerHalFactory() {} + + @Tuner.BuiltInTunerType + private int getBuiltInTunerType(Context context) { + if (mBuiltInTunerType == null) { + mBuiltInTunerType = 0; + if (CustomizationManager.hasLinuxDvbBuiltInTuner(context) + && DvbTunerHal.getNumberOfDevices(context) > 0) { + mBuiltInTunerType = Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB; + } + } + return mBuiltInTunerType; + } + + /** + * Creates a TunerHal instance. + * + * @param context context for creating the TunerHal instance + * @return the TunerHal instance + */ + @Override + @WorkerThread + public synchronized Tuner createInstance(Context context) { + Tuner tunerHal = null; + if (DvbTunerHal.getNumberOfDevices(context) > 0) { + if (DEBUG) Log.d(TAG, "Use DvbTunerHal"); + tunerHal = new DvbTunerHal(context); + } + return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null; + } + + /** + * Returns if tuner input service would use built-in tuners instead of USB tuners or network + * tuners. + */ + @Override + public boolean useBuiltInTuner(Context context) { + return getBuiltInTunerType(context) != 0; + } + + /** Gets the number of tuner devices currently present. */ + @Override + @WorkerThread + public Pair<Integer, Integer> getTunerTypeAndCount(Context context) { + if (useBuiltInTuner(context)) { + if (getBuiltInTunerType(context) == Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB) { + return new Pair<>( + Tuner.TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context)); + } + } else { + int usbTunerCount = DvbTunerHal.getNumberOfDevices(context); + if (usbTunerCount > 0) { + return new Pair<>(Tuner.TUNER_TYPE_USB, usbTunerCount); + } + } + return new Pair<>(null, 0); + } +} diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java index 84033240..4a1c7c1b 100644 --- a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java +++ b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java @@ -26,6 +26,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation; import com.android.tv.tuner.data.Cea708Data.CaptionWindow; import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; +import com.android.tv.tuner.data.Cea708Parser; import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import java.util.ArrayList; diff --git a/tuner/src/com/android/tv/tuner/data/Cea708Data.java b/tuner/src/com/android/tv/tuner/data/Cea708Data.java index 73a90181..bd1fc9b9 100644 --- a/tuner/src/com/android/tv/tuner/data/Cea708Data.java +++ b/tuner/src/com/android/tv/tuner/data/Cea708Data.java @@ -18,7 +18,6 @@ package com.android.tv.tuner.data; import android.graphics.Color; import android.support.annotation.NonNull; -import com.android.tv.tuner.cc.Cea708Parser; /** Collection of CEA-708 structures. */ public class Cea708Data { diff --git a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java index 4e080276..92834b27 100644 --- a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java +++ b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package com.android.tv.tuner.cc; +package com.android.tv.tuner.data; import android.os.SystemClock; import android.support.annotation.IntDef; import android.util.Log; import android.util.SparseIntArray; -import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionColor; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; diff --git a/tuner/src/com/android/tv/tuner/data/PsipData.java b/tuner/src/com/android/tv/tuner/data/PsipData.java index 239009dc..d4af0934 100644 --- a/tuner/src/com/android/tv/tuner/data/PsipData.java +++ b/tuner/src/com/android/tv/tuner/data/PsipData.java @@ -22,7 +22,6 @@ import android.text.format.DateUtils; import com.android.tv.common.util.StringUtils; import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.ts.SectionParser; import com.android.tv.tuner.util.ConvertUtils; import java.util.ArrayList; import java.util.HashMap; diff --git a/tuner/src/com/android/tv/tuner/ts/SectionParser.java b/tuner/src/com/android/tv/tuner/data/SectionParser.java index 27726c02..d3dba6ba 100644 --- a/tuner/src/com/android/tv/tuner/ts/SectionParser.java +++ b/tuner/src/com/android/tv/tuner/data/SectionParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.ts; +package com.android.tv.tuner.data; import android.media.tv.TvContentRating; import android.media.tv.TvContract.Programs.Genres; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java index 1f48c45b..5c203305 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java @@ -17,8 +17,8 @@ package com.android.tv.tuner.exoplayer; import android.util.Log; -import com.android.tv.tuner.cc.Cea708Parser; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; +import com.android.tv.tuner.data.Cea708Parser; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaClock; import com.google.android.exoplayer.MediaFormat; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java index e10a2991..e48cb03c 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java @@ -23,13 +23,14 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Pair; import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; @@ -49,6 +50,8 @@ import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -69,6 +72,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { private final long mId; private final Handler.Callback mSourceReaderWorker; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; private BufferManager.SampleBuffer mSampleBuffer; private Handler mSourceReaderHandler; @@ -90,7 +94,8 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { final DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, - boolean isRecording) { + boolean isRecording, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags) { this( uri, source, @@ -98,10 +103,12 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { bufferListener, isRecording, Looper.myLooper(), - new HandlerThread("SourceReaderThread")); + new HandlerThread("SourceReaderThread"), + concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags); } @VisibleForTesting + @SuppressWarnings("MissingOverride") public ExoPlayerSampleExtractor( Uri uri, DataSource source, @@ -109,9 +116,11 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { PlaybackBufferListener bufferListener, boolean isRecording, Looper workerLooper, - HandlerThread sourceReaderThread) { + HandlerThread sourceReaderThread, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { // It'll be used as a timeshift file chunk name's prefix. mId = System.currentTimeMillis(); + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; EventListener eventListener = new EventListener() { @@ -134,8 +143,19 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { // DataSource interface. return new com.google.android.exoplayer2.upstream .DataSource() { + + private @Nullable Uri uri; + + // TODO: uncomment once this is part of the public API. + // @Override + public void addTransferListener( + TransferListener transferListener) { + // Do nothing. Unsupported in V1. + } + @Override public long open(DataSpec dataSpec) throws IOException { + this.uri = dataSpec.uri; return source.open( new com.google.android.exoplayer.upstream .DataSpec( @@ -156,13 +176,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } @Override - public Uri getUri() { - return null; + public @Nullable Uri getUri() { + return uri; } @Override public void close() throws IOException { source.close(); + uri = null; } }; } @@ -176,6 +197,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { bufferManager, bufferListener, false, + mConcurrentDvrPlaybackFlags, RecordingSampleBuffer.BUFFER_REASON_RECORDING); } else { if (bufferManager == null) { @@ -186,6 +208,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { bufferManager, bufferListener, true, + mConcurrentDvrPlaybackFlags, RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); } } @@ -204,6 +227,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { private static final int RETRY_INTERVAL_MS = 50; private final MediaSource mSampleSource; + private final MediaSource.SourceInfoRefreshListener mSampleSourceListener; private MediaPeriod mMediaPeriod; private SampleStream[] mStreams; private boolean[] mTrackMetEos; @@ -215,17 +239,16 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { public SourceReaderWorker(MediaSource sampleSource) { mSampleSource = sampleSource; - mSampleSource.prepareSource( - null, - false, - new MediaSource.Listener() { + mSampleSourceListener = + new MediaSource.SourceInfoRefreshListener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, Object manifest) { // Dynamic stream change is not supported yet. b/28169263 // For now, this will cause EOS and playback reset. } - }); + }; + mSampleSource.prepareSource(null, false, mSampleSourceListener, null); mDecoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); @@ -283,11 +306,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { // This instance is already released while the extractor is preparing. return; } - TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory(); TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups(); TrackSelection[] selections = new TrackSelection[trackGroupArray.length]; for (int i = 0; i < selections.length; ++i) { - selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0); + selections[i] = new FixedTrackSelection(trackGroupArray.get(i), 0); } boolean[] retain = new boolean[trackGroupArray.length]; boolean[] reset = new boolean[trackGroupArray.length]; @@ -343,7 +365,9 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { mMediaPeriod = mSampleSource.createPeriod( new MediaSource.MediaPeriodId(0), - new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); + new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE) +// AOSP_Comment_Out , 0 + ); mMediaPeriod.prepare(this, 0); try { mMediaPeriod.maybeThrowPrepareError(); @@ -382,7 +406,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { case MSG_RELEASE: if (mMediaPeriod != null) { mSampleSource.releasePeriod(mMediaPeriod); - mSampleSource.releaseSource(); + mSampleSource.releaseSource(mSampleSourceListener); mMediaPeriod = null; } cleanUp(); @@ -607,12 +631,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { final long lastExtractedPositionUs = getLastExtractedPositionUs(); if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { mOnCompletionListenerHandler.post( - new Runnable() { - @Override - public void run() { - listener.onCompletion(result, lastExtractedPositionUs); - } - }); + () -> listener.onCompletion(result, lastExtractedPositionUs)); } } } diff --git a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java index e7224422..9749e4ba 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java @@ -18,12 +18,13 @@ package com.android.tv.tuner.exoplayer; import android.os.Handler; import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -43,10 +44,15 @@ public class FileSampleExtractor implements SampleExtractor { private final BufferManager mBufferManager; private final PlaybackBufferListener mBufferListener; private BufferManager.SampleBuffer mSampleBuffer; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; - public FileSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener) { + public FileSampleExtractor( + BufferManager bufferManager, + PlaybackBufferListener bufferListener, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { mBufferManager = bufferManager; mBufferListener = bufferListener; + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; mTrackCount = -1; } @@ -74,6 +80,7 @@ public class FileSampleExtractor implements SampleExtractor { mBufferManager, mBufferListener, true, + mConcurrentDvrPlaybackFlags, RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK); mSampleBuffer.init(ids, mTrackFormats); return true; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java index a49cbfaf..6781c616 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -31,8 +31,8 @@ import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.TunerDebug; +import com.android.tv.tuner.ts.EventDetector.EventListener; +import com.android.tv.tuner.tvinput.debug.TunerDebug; import com.google.android.exoplayer.DummyTrackRenderer; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; @@ -58,10 +58,7 @@ public class MpegTsPlayer /** Interface definition for building specific track renderers. */ public interface RendererBuilder { void buildRenderers( - MpegTsPlayer mpegTsPlayer, - DataSource dataSource, - boolean hasSoftwareAudioDecoder, - RendererBuilderCallback callback); + MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback); } /** Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */ @@ -229,7 +226,7 @@ public class MpegTsPlayer Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder, - EventDetector.EventListener eventListener) { + EventListener eventListener) { TsDataSource source = null; if (channel != null) { source = mSourceManager.createDataSource(context, channel, eventListener); @@ -246,7 +243,7 @@ public class MpegTsPlayer } mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; mBuilderCallback = new InternalRendererBuilderCallback(); - mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); + mRendererBuilder.buildRenderers(this, source, mBuilderCallback); return true; } diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java index 774285e9..e043907f 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java @@ -17,41 +17,48 @@ package com.android.tv.tuner.exoplayer; import android.content.Context; -import com.android.tv.tuner.TunerFeatures; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.google.android.exoplayer.MediaCodecSelector; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; /** Builder for renderer objects for {@link MpegTsPlayer}. */ public class MpegTsRendererBuilder implements RendererBuilder { private final Context mContext; private final BufferManager mBufferManager; private final PlaybackBufferListener mBufferListener; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; public MpegTsRendererBuilder( - Context context, BufferManager bufferManager, PlaybackBufferListener bufferListener) { + Context context, + BufferManager bufferManager, + PlaybackBufferListener bufferListener, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { mContext = context; mBufferManager = bufferManager; mBufferListener = bufferListener; + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; } @Override public void buildRenderers( - MpegTsPlayer mpegTsPlayer, - DataSource dataSource, - boolean mHasSoftwareAudioDecoder, - RendererBuilderCallback callback) { + MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback) { // Build the video and audio renderers. SampleExtractor extractor = dataSource == null - ? new MpegTsSampleExtractor(mBufferManager, mBufferListener) - : new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener); + ? new MpegTsSampleExtractor( + mBufferManager, mBufferListener, mConcurrentDvrPlaybackFlags) + : new MpegTsSampleExtractor( + dataSource, + mBufferManager, + mBufferListener, + mConcurrentDvrPlaybackFlags); SampleSource sampleSource = new MpegTsSampleSource(extractor); MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer( @@ -63,9 +70,7 @@ public class MpegTsRendererBuilder implements RendererBuilder { sampleSource, MediaCodecSelector.DEFAULT, mpegTsPlayer.getMainHandler(), - mpegTsPlayer, - mHasSoftwareAudioDecoder, - !TunerFeatures.AC3_SOFTWARE_DECODE.isEnabled(mContext)); + mpegTsPlayer); Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java index 593b576e..582f18c5 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java @@ -19,14 +19,15 @@ package com.android.tv.tuner.exoplayer; import android.net.Uri; import android.os.Handler; import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.buffer.SamplePool; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.MimeTypes; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -63,13 +64,22 @@ public final class MpegTsSampleExtractor implements SampleExtractor { * @param source the {@link DataSource} to extract from * @param bufferManager the manager for reading & writing samples backed by physical storage * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status - * change + * @param concurrentDvrPlaybackFlags */ public MpegTsSampleExtractor( - DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener) { + DataSource source, + BufferManager bufferManager, + PlaybackBufferListener bufferListener, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { + mSampleExtractor = new ExoPlayerSampleExtractor( - Uri.EMPTY, source, bufferManager, bufferListener, false); + Uri.EMPTY, + source, + bufferManager, + bufferListener, + false, + concurrentDvrPlaybackFlags); init(); } @@ -81,8 +91,11 @@ public final class MpegTsSampleExtractor implements SampleExtractor { * change */ public MpegTsSampleExtractor( - BufferManager bufferManager, PlaybackBufferListener bufferListener) { - mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener); + BufferManager bufferManager, + PlaybackBufferListener bufferListener, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { + mSampleExtractor = + new FileSampleExtractor(bufferManager, bufferListener, concurrentDvrPlaybackFlags); init(); } diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java index b136e235..c8a9c01b 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java @@ -19,7 +19,7 @@ import android.content.Context; import android.media.MediaCodec; import android.os.Handler; import android.util.Log; -import com.android.tv.tuner.TunerFeatures; +import com.android.tv.tuner.features.TunerFeatures; import com.google.android.exoplayer.DecoderInfo; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaCodecSelector; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java index 944cfbcf..bab74c9d 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java @@ -21,7 +21,7 @@ import android.os.Build; import android.os.Handler; import android.os.SystemClock; import android.util.Log; -import com.android.tv.tuner.tvinput.TunerDebug; +import com.android.tv.tuner.tvinput.debug.TunerDebug; import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaClock; @@ -106,8 +106,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me private final Handler mEventHandler; private final AudioTrackMonitor mMonitor; private final AudioClock mAudioClock; - private final boolean mAc3Passthrough; - private final boolean mSoftwareDecoderAvailable; private MediaFormat mFormat; private SampleHolder mSampleHolder; @@ -137,9 +135,7 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me SampleSource source, MediaCodecSelector selector, Handler eventHandler, - EventListener listener, - boolean hasSoftwareAudioDecoder, - boolean usePassthrough) { + EventListener listener) { mSource = source.register(); mSelector = selector; mEventHandler = eventHandler; @@ -152,9 +148,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me mMonitor = new AudioTrackMonitor(); mAudioClock = new AudioClock(); mTracksIndex = new ArrayList<>(); - mAc3Passthrough = usePassthrough; - // TODO reimplement ffmpeg decoder check for google3 - mSoftwareDecoderAvailable = false; } @Override @@ -379,19 +372,6 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me } } - private MediaFormat convertMediaFormatToRaw(MediaFormat format) { - return MediaFormat.createAudioFormat( - format.trackId, - MimeTypes.AUDIO_RAW, - format.bitrate, - format.maxInputSize, - format.durationUs, - format.channelCount, - format.sampleRate, - format.initializationData, - format.language); - } - private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { String mimeType = formatHolder.format.mimeType; mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); @@ -662,26 +642,14 @@ public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements Me if (mEventHandler == null || mEventListener == null) { return; } - mEventHandler.post( - new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackInitializationError(e); - } - }); + mEventHandler.post(() -> mEventListener.onAudioTrackInitializationError(e)); } private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { if (mEventHandler == null || mEventListener == null) { return; } - mEventHandler.post( - new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackWriteError(e); - } - }); + mEventHandler.post(() -> mEventListener.onAudioTrackWriteError(e)); } @Override diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java index b382545f..c655f779 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java @@ -69,13 +69,7 @@ public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRend private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { if (eventHandler != null && mListener != null) { - eventHandler.post( - new Runnable() { - @Override - public void run() { - mListener.onAudioTrackSetPlaybackParamsError(e); - } - }); + eventHandler.post(() -> mListener.onAudioTrackSetPlaybackParamsError(e)); } } diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java index 3e4ab103..c32540c1 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -284,6 +284,20 @@ public class BufferManager { */ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) throws IOException; + + /** + * Writes to index file to storage. + * + * @param trackName track name + * @param size size of sample + * @param position position in micro seconds + * @param sampleChunk {@link SampleChunk} chunk to be added + * @param offset offset + * @throws IOException + */ + void updateIndexFile( + String trackName, int size, long position, SampleChunk sampleChunk, int offset) + throws IOException; } private static class EvictChunkQueueMap { @@ -368,7 +382,8 @@ public class BufferManager { long positionUs, SamplePool samplePool, SampleChunk currentChunk, - int currentOffset) + int currentOffset, + boolean updateIndexFile) throws IOException { if (!maybeEvictChunk()) { throw new IOException("Not enough storage space"); @@ -386,9 +401,16 @@ public class BufferManager { mSampleChunkCreator.createSampleChunk( samplePool, file, positionUs, mChunkCallback); map.put(positionUs, new Pair(sampleChunk, 0)); + if (updateIndexFile) { + mStorageManager.updateIndexFile(id, map.size(), positionUs, sampleChunk, 0); + } return sampleChunk; } else { map.put(positionUs, new Pair(currentChunk, currentOffset)); + if (updateIndexFile) { + mStorageManager.updateIndexFile( + id, map.size(), positionUs, currentChunk, currentOffset); + } return null; } } @@ -587,6 +609,26 @@ public class BufferManager { } } + /** + * Writes track information for all tracks. + * + * @param audios list of audio track information + * @param videos list of audio track information + * @throws IOException + */ + public void writeMetaFilesOnly(List<TrackFormat> audios, List<TrackFormat> videos) + throws IOException { + if (audios.isEmpty() && videos.isEmpty()) { + throw new IOException("No track information to save"); + } + if (!audios.isEmpty()) { + mStorageManager.writeTrackInfoFiles(audios, true); + } + if (!videos.isEmpty()) { + mStorageManager.writeTrackInfoFiles(videos, false); + } + } + /** Releases all the resources. */ public void release() { try { diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java index 2a58ffcf..f19756ec 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -388,4 +389,22 @@ public class DvrStorageManager implements BufferManager.StorageManager { } } } + + @Override + public void updateIndexFile( + String trackName, int size, long position, SampleChunk sampleChunk, int offset) + throws IOException { + File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); + if (!indexFile.exists()) { + indexFile.createNewFile(); + } + RandomAccessFile accessFile = new RandomAccessFile(indexFile, "rw"); + accessFile.seek(0); + accessFile.writeLong(size); + accessFile.seek(accessFile.length()); + accessFile.writeLong(position); + accessFile.writeLong(sampleChunk.getStartPositionUs()); + accessFile.writeInt(offset); + accessFile.close(); + } } diff --git a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java index 1628bcfb..046cfbe5 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.tvinput; +package com.android.tv.tuner.exoplayer.buffer; /** The listener for buffer events occurred during playback. */ public interface PlaybackBufferListener { diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java index ebf00f59..d95642c2 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java @@ -22,12 +22,12 @@ import android.support.annotation.NonNull; import android.util.Log; import com.android.tv.tuner.exoplayer.MpegTsPlayer; import com.android.tv.tuner.exoplayer.SampleExtractor; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.util.Assertions; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -69,6 +69,7 @@ public class RecordingSampleBuffer private final BufferManager mBufferManager; private final PlaybackBufferListener mBufferListener; private final @BufferReason int mBufferReason; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; private int mTrackCount; private boolean[] mTrackSelected; @@ -103,15 +104,18 @@ public class RecordingSampleBuffer * @param bufferManager the manager of {@link SampleChunk} * @param bufferListener the listener for buffer I/O event * @param enableTrickplay {@code true} when trickplay should be enabled - * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason} + * @param concurrentDvrPlaybackFlags + * @param bufferReason the reason for caching samples {@link BufferReason} */ public RecordingSampleBuffer( BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean enableTrickplay, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, @BufferReason int bufferReason) { mBufferManager = bufferManager; mBufferListener = bufferListener; + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; if (bufferListener != null) { bufferListener.onBufferStateChanged(enableTrickplay); } @@ -129,7 +133,13 @@ public class RecordingSampleBuffer mReadSampleQueues = new ArrayList<>(); mSampleChunkIoHelper = new SampleChunkIoHelper( - ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback); + ids, + mediaFormats, + mBufferReason, + mBufferManager, + mSamplePool, + mIoCallback, + mConcurrentDvrPlaybackFlags); for (int i = 0; i < mTrackCount; ++i) { mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); } diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java index d95d0adb..f4d3bf8e 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java @@ -29,7 +29,9 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.util.MimeTypes; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.IOException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -52,6 +54,7 @@ public class SampleChunkIoHelper implements Handler.Callback { private static final int MSG_READ = 5; private static final int MSG_WRITE = 6; private static final int MSG_RELEASE = 7; + private static final int MSG_UPDATE_INDEX = 8; private final long mSampleChunkDurationUs; private final int mTrackCount; @@ -61,6 +64,7 @@ public class SampleChunkIoHelper implements Handler.Callback { private final BufferManager mBufferManager; private final SamplePool mSamplePool; private final IoCallback mIoCallback; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; private Handler mIoHandler; private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[]; @@ -70,6 +74,8 @@ public class SampleChunkIoHelper implements Handler.Callback { private final SampleChunk.IoState[] mReadIoStates; private final SampleChunk.IoState[] mWriteIoStates; private final Set<Integer> mSelectedTracks = new ArraySet<>(); + private final long[] mReadChunkOffset; + private final long[] mReadChunkPositionUs; private long mBufferDurationUs = 0; private boolean mWriteEnded; private boolean mErrorNotified; @@ -115,6 +121,7 @@ public class SampleChunkIoHelper implements Handler.Callback { * @param bufferManager manager of {@link SampleChunk} collections * @param samplePool allocator for a sample * @param ioCallback listeners for I/O events + * @param concurrentDvrPlaybackFlags */ public SampleChunkIoHelper( List<String> ids, @@ -122,7 +129,8 @@ public class SampleChunkIoHelper implements Handler.Callback { @BufferReason int bufferReason, BufferManager bufferManager, SamplePool samplePool, - IoCallback ioCallback) { + IoCallback ioCallback, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) { mTrackCount = ids.size(); mIds = ids; mMediaFormats = mediaFormats; @@ -130,11 +138,14 @@ public class SampleChunkIoHelper implements Handler.Callback { mBufferManager = bufferManager; mSamplePool = samplePool; mIoCallback = ioCallback; + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; mWriteIndexEndPositionUs = new long[mTrackCount]; mWriteChunkEndPositionUs = new long[mTrackCount]; + mReadChunkOffset = new long[mTrackCount]; + mReadChunkPositionUs = new long[mTrackCount]; mReadIoStates = new SampleChunk.IoState[mTrackCount]; mWriteIoStates = new SampleChunk.IoState[mTrackCount]; @@ -171,6 +182,29 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i)); } } + + try { + if (mConcurrentDvrPlaybackFlags.enabled() + && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING + && mTrackCount > 0) { + // Saves meta information for recording. + List<BufferManager.TrackFormat> audios = new ArrayList<>(mTrackCount); + List<BufferManager.TrackFormat> videos = new ArrayList<>(mTrackCount); + for (int i = 0; i < mTrackCount; ++i) { + android.media.MediaFormat format = + mMediaFormats.get(i).getFrameworkMediaFormatV16(); + format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs); + if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { + audios.add(new BufferManager.TrackFormat(mIds.get(i), format)); + } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { + videos.add(new BufferManager.TrackFormat(mIds.get(i), format)); + } + } + mBufferManager.writeMetaFilesOnly(audios, videos); + } + } catch (Exception e) { + Log.e(TAG, "Unable to write Meta files for DVR recording.", e); + } } /** @@ -217,6 +251,18 @@ public class SampleChunkIoHelper implements Handler.Callback { } /** + * Update Index from the specified offset. + * + * @param index track index + * @param offset of the specified position + */ + private void updateIndex(int index, long offset) { + IoParams params = + new IoParams(index, offset, null, null, null); // mReadSampleBuffers[index]); + mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_UPDATE_INDEX, params)); + } + + /** * Closes read from the specified track. * * @param index track index @@ -300,6 +346,9 @@ public class SampleChunkIoHelper implements Handler.Callback { case MSG_RELEASE: doRelease((ConditionVariable) message.obj); return true; + case MSG_UPDATE_INDEX: + doUpdateIndex((IoParams) message.obj); + return true; } } catch (IOException e) { mIoCallback.onIoError(); @@ -334,8 +383,15 @@ public class SampleChunkIoHelper implements Handler.Callback { } private void doOpenWrite(int index) throws IOException { + boolean updateIndexFile = + mConcurrentDvrPlaybackFlags.enabled() + && (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING) + && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType) + || MimeTypes.isAudio(mMediaFormats.get(index).mimeType)); + SampleChunk chunk = - mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, mSamplePool, null, 0); + mBufferManager.createNewWriteFileIfNeeded( + mIds.get(index), 0, mSamplePool, null, 0, updateIndexFile); mWriteIoStates[index].openWrite(chunk); } @@ -370,7 +426,16 @@ public class SampleChunkIoHelper implements Handler.Callback { SampleHolder sample = mReadIoStates[index].read(); if (sample != null) { mHandlerReadSampleBuffers[index].offer(sample); + if (mConcurrentDvrPlaybackFlags.enabled()) { + mReadChunkOffset[index] = mReadIoStates[index].getOffset(); + mReadChunkPositionUs[index] = sample.timeUs; + } } else { + if (mConcurrentDvrPlaybackFlags.enabled() + && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) { + // Update Index, to load new Samples + updateIndex(index, mReadChunkOffset[index]); + } // Read reached write but write is not finished yet --- wait a few moments to // see if another sample is written. mIoHandler.sendMessageDelayed( @@ -379,6 +444,27 @@ public class SampleChunkIoHelper implements Handler.Callback { } } + public void doUpdateIndex(IoParams params) throws IOException { + int index = params.index; + mIoHandler.removeMessages(MSG_READ, index); + // Update Track from Storage to load new Samples + mBufferManager.loadTrackFromStorage(mIds.get(index), mSamplePool); + Pair<SampleChunk, Integer> readPosition = + mBufferManager.getReadFile(mIds.get(index), mReadChunkPositionUs[index]); + if (readPosition == null) { + String errorMessage = + "Chunk ID:" + + mIds.get(index) + + " pos:" + + mReadChunkPositionUs[index] + + "is not found"; + SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage); + throw new IOException(errorMessage); + } + mReadIoStates[index].openRead(readPosition.first, params.positionUs); + mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index)); + } + private void doWrite(IoParams params) throws IOException { try { if (mWriteEnded) { @@ -398,13 +484,22 @@ public class SampleChunkIoHelper implements Handler.Callback { ? null : mWriteIoStates[params.index].getChunk(); int currentOffset = (int) mWriteIoStates[params.index].getOffset(); + boolean updateIndexFile = + mConcurrentDvrPlaybackFlags.enabled() + && (mBufferReason + == RecordingSampleBuffer.BUFFER_REASON_RECORDING) + && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType) + || MimeTypes.isAudio( + mMediaFormats.get(index).mimeType)); + nextChunk = mBufferManager.createNewWriteFileIfNeeded( mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool, currentChunk, - currentOffset); + currentOffset, + updateIndexFile); mWriteIndexEndPositionUs[index] = ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) * RecordingSampleBuffer.MIN_SEEK_DURATION_US; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java index 4c6260bf..843df7dc 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java @@ -20,7 +20,6 @@ import android.os.ConditionVariable; import android.support.annotation.NonNull; import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.SampleExtractor; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java index b22b8af1..3721706d 100644 --- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java @@ -142,4 +142,8 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { @Override public void writeIndexFile( String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) {} + + @Override + public void updateIndexFile( + String trackName, int size, long position, SampleChunk sampleChunk, int offset) {} } diff --git a/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java new file mode 100644 index 00000000..12039002 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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.tuner.exoplayer2; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import com.android.tv.tuner.features.TunerFeatures; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; + +/** + * Subclasses {@link MediaCodecVideoRenderer} to customize minor behaviors. + * + * <p>This class changes two behaviors from {@link MediaCodecVideoRenderer}: + * + * <ul> + * <li>Prefer software decoders for sub-HD streams. + * <li>Prevents the rendering of the first frame when audio can start playing before the first + * video key frame's presentation timestamp. + * </ul> + */ +public class VideoRendererExoV2 extends MediaCodecVideoRenderer { + private static final String TAG = "MpegTsVideoTrackRender"; + + private static final String SOFTWARE_DECODER_NAME_PREFIX = "OMX.google."; + private static final long ALLOWED_JOINING_TIME_MS = 5000; + private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10; + private static final int MIN_HD_HEIGHT = 720; + private static Field sRenderedFirstFrameField; + + private final boolean mIsSwCodecEnabled; + private boolean mCodecIsSwPreferred; + private boolean mSetRenderedFirstFrame; + + static { + // Remove the reflection below once b/31223646 is resolved. + try { + // TODO: Remove this workaround by using public notification mechanisms. + sRenderedFirstFrameField = + MediaCodecVideoRenderer.class.getDeclaredField("renderedFirstFrame"); + sRenderedFirstFrameField.setAccessible(true); + } catch (NoSuchFieldException e) { + // Null-checking for {@code sRenderedFirstFrameField} will do the error handling. + } + } + + /** + * Creates an instance. + * + * @param context A context. + * @param handler The handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param listener The listener of events. May be null if delivery of events is not required. + */ + public VideoRendererExoV2( + Context context, Handler handler, VideoRendererEventListener listener) { + super( + context, + MediaCodecSelector.DEFAULT, + ALLOWED_JOINING_TIME_MS, + handler, + listener, + DROPPED_FRAMES_NOTIFICATION_THRESHOLD); + mIsSwCodecEnabled = TunerFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context); + } + + @Override + protected List<MediaCodecInfo> getDecoderInfos( + MediaCodecSelector codecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { + List<MediaCodecInfo> decoderInfos = + super.getDecoderInfos(codecSelector, format, requiresSecureDecoder); + if (mIsSwCodecEnabled && mCodecIsSwPreferred) { + // If software decoders are preferred, we sort the returned list so that software + // decoders appear first. + Collections.sort( + decoderInfos, + (o1, o2) -> + // Negate the result to consider software decoders as lower in + // comparisons. + -Boolean.compare( + o1.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX), + o2.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX))); + } + return decoderInfos; + } + + @Override + protected void onInputFormatChanged(Format format) throws ExoPlaybackException { + mCodecIsSwPreferred = + MimeTypes.VIDEO_MPEG2.equals(format.sampleMimeType) + && format.height < MIN_HD_HEIGHT; + super.onInputFormatChanged(format); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + // Disabling pre-rendering of the first frame in order to avoid a frozen picture when + // starting the playback. We do this only once, when the renderer is enabled at first, since + // we need to pre-render the frame in advance when we do trickplay backed by seeking. + if (!mSetRenderedFirstFrame) { + setRenderedFirstFrame(true); + mSetRenderedFirstFrame = true; + } + } + + private void setRenderedFirstFrame(boolean renderedFirstFrame) { + if (sRenderedFirstFrameField != null) { + try { + sRenderedFirstFrameField.setBoolean(this, renderedFirstFrame); + } catch (IllegalAccessException e) { + Log.w( + TAG, + "renderedFirstFrame is not accessible. Playback may start with a frozen" + + " picture."); + } + } + } +} diff --git a/tuner/src/com/android/tv/tuner/features/TunerFeatures.java b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java new file mode 100644 index 00000000..6033a3a6 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 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.tuner.features; + +import static com.android.tv.common.feature.FeatureUtils.OFF; + +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.feature.Feature; +import com.android.tv.common.feature.Model; +import com.android.tv.common.feature.PropertyFeature; +import com.android.tv.common.feature.Sdk; + +/** + * List of {@link Feature} for Tuner. + * + * <p>Only for use in Tuners. + * + * <p>Remove the {@code Feature} once it is launched. + */ +public class TunerFeatures extends CommonFeatures { + + /** + * USE_SW_CODEC_FOR_SD + * + * <p>Prefer software based codec for SD channels. + */ + public static final Feature USE_SW_CODEC_FOR_SD = + PropertyFeature.create( + "use_sw_codec_for_sd", + false + ); + + /** + * Does the TvProvider on the installed device allow systems inserts to the programs table. + * + * <p>This is available in {@link Sdk#AT_LEAST_O} but vendors may choose to backport support to + * the TvProvider. + */ + public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION = Sdk.AT_LEAST_O; + + /** Enable Dvb parsers and listeners. */ + public static final Feature ENABLE_FILE_DVB = OFF; + + private TunerFeatures() {} +} diff --git a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java index dd92b641..6d17be98 100644 --- a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java +++ b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java @@ -35,14 +35,11 @@ public class ScaledLayout extends ViewGroup { private static final String TAG = "ScaledLayout"; private static final boolean DEBUG = false; private static final Comparator<Rect> mRectTopLeftSorter = - new Comparator<Rect>() { - @Override - public int compare(Rect lhs, Rect rhs) { - if (lhs.top != rhs.top) { - return lhs.top - rhs.top; - } else { - return lhs.left - rhs.left; - } + (Rect lhs, Rect rhs) -> { + if (lhs.top != rhs.top) { + return lhs.top - rhs.top; + } else { + return lhs.left - rhs.left; } }; diff --git a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java index f741fdb0..92701db8 100644 --- a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java +++ b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java @@ -17,6 +17,18 @@ package com.android.tv.tuner.livetuner; import com.android.tv.tuner.tvinput.BaseTunerTvInputService; +import dagger.android.ContributesAndroidInjector; /** Live TV embedded tuner. */ -public class LiveTvTunerTvInputService extends BaseTunerTvInputService {} +public class LiveTvTunerTvInputService extends BaseTunerTvInputService { + + /** + * Exports {@link LiveTvTunerTvInputService} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract LiveTvTunerTvInputService contributesLiveTvTunerTvInputServiceInjector(); + } +} diff --git a/tuner/src/com/android/tv/tuner/modules/TunerModule.java b/tuner/src/com/android/tv/tuner/modules/TunerModule.java new file mode 100644 index 00000000..4843f383 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/modules/TunerModule.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 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.tuner.modules; + +import com.android.tv.tuner.source.TunerSourceModule; +import dagger.Module; + +/** Dagger module for TV Tuners. */ +@Module(includes = {TunerSingletonsModule.class, TunerSourceModule.class}) +public class TunerModule {} diff --git a/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java new file mode 100644 index 00000000..b7fba8d2 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java @@ -0,0 +1,18 @@ +package com.android.tv.tuner.modules; + +import com.android.tv.tuner.singletons.TunerSingletons; +import dagger.Module; + +/** + * Provides bindings for items provided by {@link TunerSingletons}. + * + * <p>Use this module to inject items directly instead of using {@code TunerSingletons}. + */ +@Module +public class TunerSingletonsModule { + private final TunerSingletons mTunerSingletons; + + public TunerSingletonsModule(TunerSingletons tunerSingletons) { + this.mTunerSingletons = tunerSingletons; + } +} diff --git a/tuner/src/com/android/tv/tuner/TunerPreferences.java b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java index 7b45b997..85e3a5ec 100644 --- a/tuner/src/com/android/tv/tuner/TunerPreferences.java +++ b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner; +package com.android.tv.tuner.prefs; import android.content.Context; import android.content.SharedPreferences; diff --git a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java index 1be4e1c2..44f689bf 100644 --- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java +++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java @@ -22,6 +22,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; @@ -30,16 +31,13 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; -import com.android.tv.common.BaseApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.experiments.Experiments; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupFragment; @@ -47,12 +45,14 @@ import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.common.util.AutoCloseableUtils; import com.android.tv.common.util.PostalCodeUtils; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.api.TunerFactory; +import com.android.tv.tuner.prefs.TunerPreferences; import java.util.concurrent.Executor; +import javax.inject.Inject; /** The base setup activity class for tuner. */ -public class BaseTunerSetupActivity extends SetupActivity { +public abstract class BaseTunerSetupActivity extends SetupActivity { private static final String TAG = "BaseTunerSetupActivity"; private static final boolean DEBUG = false; @@ -86,27 +86,21 @@ public class BaseTunerSetupActivity extends SetupActivity { protected String mPreviousPostalCode; protected boolean mActivityStopped; protected boolean mPendingShowInitialFragment; + @Inject protected TunerFactory mTunerFactory; - private TunerHalFactory mTunerHalFactory; + private TunerHalCreator mTunerHalCreator; @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) { Log.d(TAG, "onCreate"); } + super.onCreate(savedInstanceState); mActivityStopped = false; executeGetTunerTypeAndCountAsyncTask(); - mTunerHalFactory = - new TunerHalFactory(getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR); - super.onCreate(savedInstanceState); - // TODO: check {@link shouldShowRequestPermissionRationale}. - if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - // No need to check the request result. - requestPermissions( - new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); - } + mTunerHalCreator = + new TunerHalCreator( + getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR, mTunerFactory); try { // Updating postal code takes time, therefore we called it here for "warm-up". mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this); @@ -138,25 +132,6 @@ public class BaseTunerSetupActivity extends SetupActivity { } @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED - && Experiments.CLOUD_EPG.get()) { - try { - // Updating postal code takes time, therefore we should update postal code - // right after the permission is granted, so that the subsequent operations, - // especially EPG fetcher, could get the newly updated postal code. - PostalCodeUtils.updatePostalCode(this); - } catch (Exception e) { - // Do nothing - } - } - } - } - - @Override protected Fragment onCreateInitialFragment() { if (mTunerType != null) { SetupFragment fragment = new WelcomeFragment(); @@ -184,10 +159,16 @@ public class BaseTunerSetupActivity extends SetupActivity { break; default: String postalCode = PostalCodeUtils.getLastPostalCode(this); - if (mNeedToShowPostalCodeFragment - || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( + boolean needLocation = + CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled( getApplicationContext()) - && TextUtils.isEmpty(postalCode))) { + && TextUtils.isEmpty(postalCode); + if (needLocation + && checkSelfPermission( + android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + showLocationFragment(); + } else if (mNeedToShowPostalCodeFragment || needLocation) { // We cannot get postal code automatically. Postal code input fragment // should always be shown even if users have input some valid postal // code in this activity before. @@ -199,6 +180,23 @@ public class BaseTunerSetupActivity extends SetupActivity { break; } return true; + case LocationFragment.ACTION_CATEGORY: + switch (actionId) { + case LocationFragment.ACTION_ALLOW_PERMISSION: + String postalCode = + params == null + ? null + : params.getString(LocationFragment.KEY_POSTAL_CODE); + if (postalCode == null) { + showPostalCodeFragment(); + } else { + showConnectionTypeFragment(); + } + break; + default: + showConnectionTypeFragment(); + } + return true; case PostalCodeFragment.ACTION_CATEGORY: switch (actionId) { case SetupMultiPaneFragment.ACTION_DONE: @@ -210,7 +208,7 @@ public class BaseTunerSetupActivity extends SetupActivity { } return true; case ConnectionTypeFragment.ACTION_CATEGORY: - if (mTunerHalFactory.getOrCreate() == null) { + if (mTunerHalCreator.getOrCreate() == null) { finish(); Toast.makeText( getApplicationContext(), @@ -233,7 +231,7 @@ public class BaseTunerSetupActivity extends SetupActivity { getFragmentManager().popBackStack(); return true; case ScanFragment.ACTION_FINISH: - mTunerHalFactory.clear(); + mTunerHalCreator.clear(); showScanResultFragment(); return true; default: // fall out @@ -269,22 +267,36 @@ public class BaseTunerSetupActivity extends SetupActivity { } /** Gets the currently used tuner HAL. */ - TunerHal getTunerHal() { - return mTunerHalFactory.getOrCreate(); + Tuner getTunerHal() { + return mTunerHalCreator.getOrCreate(); } /** Generates tuner HAL. */ void generateTunerHal() { - mTunerHalFactory.generate(); + mTunerHalCreator.generate(); } /** Clears the currently used tuner HAL. */ protected void clearTunerHal() { - mTunerHalFactory.clear(); + mTunerHalCreator.clear(); + } + + protected void showLocationFragment() { + SetupFragment fragment = new LocationFragment(); + fragment.setShortDistance( + SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); + showFragment(fragment, true); } protected void showPostalCodeFragment() { + showPostalCodeFragment(null); + } + + protected void showPostalCodeFragment(Bundle args) { SetupFragment fragment = new PostalCodeFragment(); + if (args != null) { + fragment.setArguments(args); + } fragment.setShortDistance( SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION); showFragment(fragment, true); @@ -320,25 +332,28 @@ public class BaseTunerSetupActivity extends SetupActivity { /** * A callback to be invoked when the TvInputService is enabled or disabled. * + * @param tunerSetupIntent * @param context a {@link Context} instance * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; otherwise * {@code false} */ - public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) { + public static void onTvInputEnabled( + Context context, boolean enabled, Integer tunerType, Intent tunerSetupIntent) { // Send a notification for tuner setup if there's no channels and the tuner TV input // setup has been not done. boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context); int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context); if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) { TunerPreferences.setShouldShowSetupActivity(context, true); - sendNotification(context, tunerType); + sendNotification(context, tunerType, tunerSetupIntent); } else { TunerPreferences.setShouldShowSetupActivity(context, false); cancelNotification(context); } } - private static void sendNotification(Context context, Integer tunerType) { + private static void sendNotification( + Context context, Integer tunerType, Intent tunerSetupIntent) { SoftPreconditions.checkState( tunerType != null, TAG, "tunerType is null when send notification"); if (tunerType == null) { @@ -348,29 +363,29 @@ public class BaseTunerSetupActivity extends SetupActivity { String contentTitle = resources.getString(R.string.ut_setup_notification_content_title); int contentTextId = 0; switch (tunerType) { - case TunerHal.TUNER_TYPE_BUILT_IN: + case Tuner.TUNER_TYPE_BUILT_IN: contentTextId = R.string.bt_setup_notification_content_text; break; - case TunerHal.TUNER_TYPE_USB: + case Tuner.TUNER_TYPE_USB: contentTextId = R.string.ut_setup_notification_content_text; break; - case TunerHal.TUNER_TYPE_NETWORK: + case Tuner.TUNER_TYPE_NETWORK: contentTextId = R.string.nt_setup_notification_content_text; break; default: // fall out } String contentText = resources.getString(contentTextId); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - sendNotificationInternal(context, contentTitle, contentText); + sendNotificationInternal(context, contentTitle, contentText, tunerSetupIntent); } else { Bitmap largeIcon = BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna); - sendRecommendationCard(context, contentTitle, contentText, largeIcon); + sendRecommendationCard(context, contentTitle, contentText, largeIcon, tunerSetupIntent); } } private static void sendNotificationInternal( - Context context, String contentTitle, String contentText) { + Context context, String contentTitle, String contentText, Intent tunerSetupIntent) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel( @@ -387,7 +402,8 @@ public class BaseTunerSetupActivity extends SetupActivity { context.getResources() .getIdentifier( TAG_ICON, TAG_DRAWABLE, context.getPackageName())) - .setContentIntent(createPendingIntentForSetupActivity(context)) + .setContentIntent( + createPendingIntentForSetupActivity(context, tunerSetupIntent)) .setVisibility(Notification.VISIBILITY_PUBLIC) .extend(new Notification.TvExtender()) .build(); @@ -397,10 +413,15 @@ public class BaseTunerSetupActivity extends SetupActivity { /** * Sends the recommendation card to start the tuner TV input setup activity. * + * @param tunerSetupIntent * @param context a {@link Context} instance */ private static void sendRecommendationCard( - Context context, String contentTitle, String contentText, Bitmap largeIcon) { + Context context, + String contentTitle, + String contentText, + Bitmap largeIcon, + Intent tunerSetupIntent) { // Build and send the notification. Notification notification = new NotificationCompat.BigPictureStyle( @@ -418,7 +439,8 @@ public class BaseTunerSetupActivity extends SetupActivity { TAG_DRAWABLE, context.getPackageName())) .setContentIntent( - createPendingIntentForSetupActivity(context))) + createPendingIntentForSetupActivity( + context, tunerSetupIntent))) .build(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); @@ -429,30 +451,27 @@ public class BaseTunerSetupActivity extends SetupActivity { * Returns a {@link PendingIntent} to launch the tuner TV input service. * * @param context a {@link Context} instance + * @param tunerSetupIntent */ - private static PendingIntent createPendingIntentForSetupActivity(Context context) { + private static PendingIntent createPendingIntentForSetupActivity( + Context context, Intent tunerSetupIntent) { return PendingIntent.getActivity( - context, - 0, - BaseApplication.getSingletons(context).getTunerSetupIntent(context), - PendingIntent.FLAG_UPDATE_CURRENT); + context, 0, tunerSetupIntent, PendingIntent.FLAG_UPDATE_CURRENT); } - /** A static factory for {@link TunerHal} instances * */ + /** Creates {@link Tuner} instances in a worker thread * */ @VisibleForTesting - protected static class TunerHalFactory { + protected static class TunerHalCreator { private Context mContext; - @VisibleForTesting TunerHal mTunerHal; - private TunerHalFactory.GenerateTunerHalTask mGenerateTunerHalTask; + @VisibleForTesting Tuner mTunerHal; + private TunerHalCreator.GenerateTunerHalTask mGenerateTunerHalTask; private final Executor mExecutor; + private final TunerFactory mTunerFactory; - TunerHalFactory(Context context) { - this(context, AsyncTask.SERIAL_EXECUTOR); - } - - TunerHalFactory(Context context, Executor executor) { + TunerHalCreator(Context context, Executor executor, TunerFactory tunerFactory) { mContext = context; mExecutor = executor; + mTunerFactory = tunerFactory; } /** @@ -460,7 +479,7 @@ public class BaseTunerSetupActivity extends SetupActivity { * before, tries to generate it synchronously. */ @WorkerThread - TunerHal getOrCreate() { + Tuner getOrCreate() { if (mGenerateTunerHalTask != null && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { try { @@ -478,7 +497,7 @@ public class BaseTunerSetupActivity extends SetupActivity { @MainThread void generate() { if (mGenerateTunerHalTask == null && mTunerHal == null) { - mGenerateTunerHalTask = new TunerHalFactory.GenerateTunerHalTask(); + mGenerateTunerHalTask = new TunerHalCreator.GenerateTunerHalTask(); mGenerateTunerHalTask.executeOnExecutor(mExecutor); } } @@ -497,18 +516,18 @@ public class BaseTunerSetupActivity extends SetupActivity { } @WorkerThread - protected TunerHal createInstance() { - return TunerHal.createInstance(mContext); + protected Tuner createInstance() { + return mTunerFactory.createInstance(mContext); } - class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> { + class GenerateTunerHalTask extends AsyncTask<Void, Void, Tuner> { @Override - protected TunerHal doInBackground(Void... args) { + protected Tuner doInBackground(Void... args) { return createInstance(); } @Override - protected void onPostExecute(TunerHal tunerHal) { + protected void onPostExecute(Tuner tunerHal) { mTunerHal = tunerHal; } } diff --git a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java index d2ed6c38..43c584ed 100644 --- a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java +++ b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.tv.tuner; +package com.android.tv.tuner.setup; import android.util.Log; -import com.android.tv.tuner.data.nano.Channel; +import com.android.tv.tuner.api.ScanChannel; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -26,49 +26,9 @@ import java.util.ArrayList; import java.util.List; /** Parses plain text formatted scan files, which contain the list of channels. */ -public class ChannelScanFileParser { +public final class ChannelScanFileParser { private static final String TAG = "ChannelScanFileParser"; - public static final class ScanChannel { - public final int type; - public final int frequency; - public final String modulation; - public final String filename; - /** - * Radio frequency (channel) number specified at - * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code - * null} for cases like cable signal. - */ - public final Integer radioFrequencyNumber; - - public static ScanChannel forTuner( - int frequency, String modulation, Integer radioFrequencyNumber) { - return new ScanChannel( - Channel.TunerType.TYPE_TUNER, - frequency, - modulation, - null, - radioFrequencyNumber); - } - - public static ScanChannel forFile(int frequency, String filename) { - return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null); - } - - private ScanChannel( - int type, - int frequency, - String modulation, - String filename, - Integer radioFrequencyNumber) { - this.type = type; - this.frequency = frequency; - this.modulation = modulation; - this.filename = filename; - this.radioFrequencyNumber = radioFrequencyNumber; - } - } - /** * Parses a given scan file and returns the list of {@link ScanChannel} objects. * @@ -105,4 +65,6 @@ public class ChannelScanFileParser { } return scanChannelList; } + + private ChannelScanFileParser(){} } diff --git a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java index 722de7c6..741edc78 100644 --- a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java +++ b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java @@ -17,20 +17,37 @@ package com.android.tv.tuner.setup; import android.app.FragmentManager; +import android.content.pm.PackageManager; import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; import android.view.KeyEvent; -import com.android.tv.tuner.TunerHal; +import com.android.tv.common.util.PostalCodeUtils; +import dagger.android.ContributesAndroidInjector; /** An activity that serves tuner setup process. */ public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity { private static final String TAG = "LiveTvTunerSetupActivity"; @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // TODO(shubang): use LocationFragment + if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + // No need to check the request result. + requestPermissions( + new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, + PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); + } + } + + @Override protected void executeGetTunerTypeAndCountAsyncTask() { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... arg0) { - return TunerHal.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first; + return mTunerFactory.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first; } @Override @@ -72,4 +89,31 @@ public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity { } return super.onKeyUp(keyCode, event); } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + try { + // Updating postal code takes time, therefore we should update postal code + // right after the permission is granted, so that the subsequent operations, + // especially EPG fetcher, could get the newly updated postal code. + PostalCodeUtils.updatePostalCode(this); + } catch (Exception e) { + // Do nothing + } + } + } + } + + /** + * Exports {@link LiveTvTunerSetupActivity} for Dagger codegen to create the appropriate + * injector. + */ + @dagger.Module + public abstract static class Module { + @ContributesAndroidInjector + abstract LiveTvTunerSetupActivity contributeLiveTvTunerSetupActivityInjector(); + } } diff --git a/tuner/src/com/android/tv/tuner/setup/LocationFragment.java b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java new file mode 100644 index 00000000..1234ae20 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2018 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.tuner.setup; + +import static com.android.tv.tuner.setup.BaseTunerSetupActivity.PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION; + +import android.content.pm.PackageManager; +import android.location.Address; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.util.Log; + +import com.android.tv.common.ui.setup.SetupActionHelper; +import com.android.tv.common.ui.setup.SetupGuidedStepFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.common.util.LocationUtils; +import com.android.tv.tuner.R; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** A fragment shows the rationale of location permission */ +public class LocationFragment extends SetupMultiPaneFragment { + private static final String TAG = "com.android.tv.tuner.setup.LocationFragment"; + private static final boolean DEBUG = true; + + public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.LocationFragment"; + public static final String KEY_POSTAL_CODE = "key_postal_code"; + + public static final int ACTION_ALLOW_PERMISSION = 1; + public static final int ENTER_ZIP_CODE = 2; + public static final int ACTION_GETTING_LOCATION = 3; + public static final int GET_LOCATION_TIMEOUT_MS = 3000; + + @Override + protected SetupGuidedStepFragment onCreateContentFragment() { + return new ContentFragment(); + } + + @Override + protected String getActionCategory() { + return ACTION_CATEGORY; + } + + @Override + protected boolean needsDoneButton() { + return false; + } + + /** The content fragment of {@link LocationFragment}. */ + public static class ContentFragment extends SetupGuidedStepFragment + implements LocationUtils.OnUpdateAddressListener { + private final List<GuidedAction> mGettingLocationAction = new ArrayList<>(); + private final Handler mHandler = new Handler(); + private final Object mPostalCodeLock = new Object(); + + private String mPostalCode; + private boolean mPermissionGranted; + + private final Runnable mTimeoutRunnable = + () -> { + synchronized (mPostalCodeLock) { + if (DEBUG) { + Log.d(TAG, + "get location timeout. mPostalCode=" + mPostalCode); + } + if (mPostalCode == null) { + // timeout. setup activity will get null postal code + LocationUtils.removeOnUpdateAddressListener(this); + passPostalCode(); + } + } + }; + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.location_guidance_title); + String description = getString(R.string.location_guidance_description); + return new Guidance(title, description, getString(R.string.ut_setup_breadcrumb), null); + } + + @Override + public void onCreateActions( + @NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ALLOW_PERMISSION) + .title(getString(R.string.location_choices_allow_permission)) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ENTER_ZIP_CODE) + .title(getString(R.string.location_choices_enter_zip_code)) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_SKIP) + .title(getString(com.android.tv.common.R.string.action_text_skip)) + .build()); + mGettingLocationAction.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_GETTING_LOCATION) + .title(getString(R.string.location_choices_getting_location)) + .focusable(false) + .build() + ); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (DEBUG) { + Log.d(TAG, "onGuidedActionClicked. Action ID = " + action.getId()); + } + if (action.getId() == ACTION_ALLOW_PERMISSION) { + // request permission when users click this action + mPermissionGranted = false; + requestPermissions( + new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, + PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); + } else { + super.onGuidedActionClicked(action); + } + } + + @Override + protected String getActionCategory() { + return ACTION_CATEGORY; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + synchronized (mPostalCodeLock) { + mPermissionGranted = true; + if (mPostalCode == null) { + // get postal code immediately if available + try { + Address address = LocationUtils.getCurrentAddress(getActivity()); + if (address != null) { + mPostalCode = address.getPostalCode(); + } + } catch (IOException e) { + // do nothing + } + } + if (DEBUG) { + Log.d(TAG, "permission granted. mPostalCode=" + mPostalCode); + } + if (mPostalCode != null) { + // if postal code is known, pass it the setup activity + LocationUtils.removeOnUpdateAddressListener(this); + passPostalCode(); + } else { + // show "getting location" message + setActions(mGettingLocationAction); + // post timeout runnable + mHandler.postDelayed(mTimeoutRunnable, GET_LOCATION_TIMEOUT_MS); + } + } + } + } + } + + @Override + public boolean onUpdateAddress(Address address) { + synchronized (mPostalCodeLock) { + // it takes time to get location after the permission is granted, + // so this listener is needed + mPostalCode = address.getPostalCode(); + if (DEBUG) { + Log.d(TAG, "onUpdateAddress. mPostalCode=" + mPostalCode); + } + if (mPermissionGranted && mPostalCode != null) { + // pass the postal code only if permission is granted + passPostalCode(); + return true; + } + return false; + } + } + + @Override + public void onResume() { + if (DEBUG) { + Log.d(TAG, "onResume"); + } + super.onResume(); + LocationUtils.addOnUpdateAddressListener(this); + } + + @Override + public void onPause() { + if (DEBUG) { + Log.d(TAG, "onPause"); + } + LocationUtils.removeOnUpdateAddressListener(this); + mHandler.removeCallbacks(mTimeoutRunnable); + super.onPause(); + } + + private void passPostalCode() { + synchronized (mPostalCodeLock) { + mHandler.removeCallbacks(mTimeoutRunnable); + Bundle params = new Bundle(); + if (mPostalCode != null) { + params.putString(KEY_POSTAL_CODE, mPostalCode); + } + SetupActionHelper.onActionClick( + this, ACTION_CATEGORY, ACTION_ALLOW_PERMISSION, params); + } + } + } +} diff --git a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java index f4b9f65e..52247972 100644 --- a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java +++ b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java @@ -32,10 +32,11 @@ import com.android.tv.common.util.PostalCodeUtils; import com.android.tv.tuner.R; import java.util.List; -/** A fragment for initial screen. */ +/** A fragment for users to enter postal code. */ public class PostalCodeFragment extends SetupMultiPaneFragment { public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.PostalCodeFragment"; public static final String KEY_POSTAL_CODE = "postal_code"; + public static final String KEY_GET_LOCATION_FAILED = "get_location_failed"; private static final int VIEW_TYPE_EDITABLE = 1; @Override @@ -43,6 +44,11 @@ public class PostalCodeFragment extends SetupMultiPaneFragment { ContentFragment fragment = new ContentFragment(); Bundle arguments = new Bundle(); arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); + if (getArguments() != null) { + arguments.putBoolean( + KEY_GET_LOCATION_FAILED, + getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false)); + } fragment.setArguments(arguments); return fragment; } @@ -139,9 +145,16 @@ public class PostalCodeFragment extends SetupMultiPaneFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.postal_code_guidance_title); - String description = getString(R.string.postal_code_guidance_description); + StringBuilder description = new StringBuilder(); + if (getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false)) { + description + .append(getString(R.string + .postal_code_guidance_description_get_location_failed)) + .append(" "); + } + description.append(getString(R.string.postal_code_guidance_description)); String breadcrumb = getString(R.string.ut_setup_breadcrumb); - return new Guidance(title, description, breadcrumb, null); + return new Guidance(title, description.toString(), breadcrumb, null); } @Override diff --git a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java index 3ac86e19..7d59284c 100644 --- a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java +++ b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java @@ -37,21 +37,21 @@ import android.widget.ProgressBar; import android.widget.TextView; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.ui.setup.SetupFragment; -import com.android.tv.tuner.ChannelScanFileParser; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.api.ScanChannel; +import com.android.tv.tuner.api.Tuner; import com.android.tv.tuner.data.PsipData; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.data.nano.Channel; +import com.android.tv.tuner.prefs.TunerPreferences; import com.android.tv.tuner.source.FileTsStreamer; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsStreamer; import com.android.tv.tuner.source.TunerTsStreamer; -import com.android.tv.tuner.tvinput.ChannelDataManager; -import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.ts.EventDetector; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -99,7 +99,7 @@ public class ScanFragment extends SetupFragment { if (DEBUG) Log.d(TAG, "onCreateView"); View view = super.onCreateView(inflater, container, savedInstanceState); mChannelNumbers = new ArrayList<>(); - mChannelDataManager = new ChannelDataManager(getActivity()); + mChannelDataManager = new ChannelDataManager(getActivity().getApplicationContext()); mChannelDataManager.checkDataVersion(getActivity()); mAdapter = new ChannelAdapter(); mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress); @@ -126,10 +126,10 @@ public class ScanFragment extends SetupFragment { startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title); switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: + case Tuner.TUNER_TYPE_USB: scanTitleView.setText(R.string.ut_channel_scan); break; - case TunerHal.TUNER_TYPE_NETWORK: + case Tuner.TUNER_TYPE_NETWORK: scanTitleView.setText(R.string.nt_channel_scan); break; default: @@ -176,12 +176,9 @@ public class ScanFragment extends SetupFragment { // Notifies a user of waiting to finish the scanning process. new Handler() .postDelayed( - new Runnable() { - @Override - public void run() { - if (mChannelScanTask != null) { - mChannelScanTask.showFinishingProgressDialog(); - } + () -> { + if (mChannelScanTask != null) { + mChannelScanTask.showFinishingProgressDialog(); } }, SHOW_PROGRESS_DIALOG_DELAY_MS); @@ -248,7 +245,7 @@ public class ScanFragment extends SetupFragment { } private class ChannelScanTask extends AsyncTask<Void, Integer, Void> - implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener { + implements EventDetector.EventListener, ChannelDataManager.ChannelHandlingDoneListener { private static final int MAX_PROGRESS = 100; private final Activity mActivity; @@ -257,7 +254,7 @@ public class ScanFragment extends SetupFragment { private final TsStreamer mFileTsStreamer; private final ConditionVariable mConditionStopped; - private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>(); + private final List<ScanChannel> mScanChannelList = new ArrayList<>(); private boolean mIsCanceled; private boolean mIsFinished; private ProgressDialog mFinishingProgressDialog; @@ -269,7 +266,7 @@ public class ScanFragment extends SetupFragment { if (FAKE_MODE) { mScanTsStreamer = new FakeTsStreamer(this); } else { - TunerHal hal = ((BaseTunerSetupActivity) mActivity).getTunerHal(); + Tuner hal = ((BaseTunerSetupActivity) mActivity).getTunerHal(); if (hal == null) { throw new RuntimeException("Failed to open a DVB device"); } @@ -282,41 +279,35 @@ public class ScanFragment extends SetupFragment { private void maybeSetChannelListVisible() { mActivity.runOnUiThread( - new Runnable() { - @Override - public void run() { - int channelsFound = mAdapter.getCount(); - if (!mChannelListVisible && channelsFound > 0) { - String format = - getResources() - .getQuantityString( - R.plurals.ut_channel_scan_message, - channelsFound, - channelsFound); - mScanningMessage.setText(String.format(format, channelsFound)); - mChannelHolder.setVisibility(View.VISIBLE); - mChannelListVisible = true; - } + () -> { + int channelsFound = mAdapter.getCount(); + if (!mChannelListVisible && channelsFound > 0) { + String format = + getResources() + .getQuantityString( + R.plurals.ut_channel_scan_message, + channelsFound, + channelsFound); + mScanningMessage.setText(String.format(format, channelsFound)); + mChannelHolder.setVisibility(View.VISIBLE); + mChannelListVisible = true; } }); } private void addChannel(final TunerChannel channel) { mActivity.runOnUiThread( - new Runnable() { - @Override - public void run() { - mAdapter.add(channel); - if (mChannelListVisible) { - int channelsFound = mAdapter.getCount(); - String format = - getResources() - .getQuantityString( - R.plurals.ut_channel_scan_message, - channelsFound, - channelsFound); - mScanningMessage.setText(String.format(format, channelsFound)); - } + () -> { + mAdapter.add(channel); + if (mChannelListVisible) { + int channelsFound = mAdapter.getCount(); + String format = + getResources() + .getQuantityString( + R.plurals.ut_channel_scan_message, + channelsFound, + channelsFound); + mScanningMessage.setText(String.format(format, channelsFound)); } }); } @@ -366,7 +357,7 @@ public class ScanFragment extends SetupFragment { long startMs = System.currentTimeMillis(); int i = 1; - for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) { + for (ScanChannel scanChannel : mScanChannelList) { int frequency = scanChannel.frequency; String modulation = scanChannel.modulation; Log.i(TAG, "Tuning to " + frequency + " " + modulation); @@ -403,7 +394,7 @@ public class ScanFragment extends SetupFragment { if (DEBUG) Log.i(TAG, "Channel scan ended"); } - private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) { + private void addChannelsWithoutVct(ScanChannel scanChannel) { if (scanChannel.radioFrequencyNumber == null || !(mScanTsStreamer instanceof TunerTsStreamer)) { return; @@ -515,7 +506,7 @@ public class ScanFragment extends SetupFragment { } @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { + public boolean startStream(ScanChannel channel) { if (++mProgramNumber % 2 == 1) { return true; } diff --git a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java index 480bf081..bd3f9ad9 100644 --- a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java @@ -25,11 +25,11 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.prefs.TunerPreferences; import java.util.List; -/** A fragment for initial screen. */ +/** A fragment to show found channels. */ public class ScanResultFragment extends SetupMultiPaneFragment { public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanResultFragment"; @@ -83,10 +83,10 @@ public class ScanResultFragment extends SetupMultiPaneFragment { (args == null ? 0 : args.getInt(BaseTunerSetupActivity.KEY_TUNER_TYPE, 0)); title = getString(R.string.ut_result_not_found_title); switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: + case Tuner.TUNER_TYPE_USB: description = getString(R.string.ut_result_not_found_description); break; - case TunerHal.TUNER_TYPE_NETWORK: + case Tuner.TUNER_TYPE_NETWORK: description = getString(R.string.nt_result_not_found_description); break; default: diff --git a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java index 788ba918..2a414df7 100644 --- a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java +++ b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java @@ -24,8 +24,8 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.prefs.TunerPreferences; import java.util.List; /** A fragment for initial screen. */ @@ -69,14 +69,14 @@ public class WelcomeFragment extends SetupMultiPaneFragment { getArguments() .getInt( BaseTunerSetupActivity.KEY_TUNER_TYPE, - TunerHal.TUNER_TYPE_BUILT_IN); + Tuner.TUNER_TYPE_BUILT_IN); if (mChannelCountOnPreference == 0) { switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: + case Tuner.TUNER_TYPE_USB: title = getString(R.string.ut_setup_new_title); description = getString(R.string.ut_setup_new_description); break; - case TunerHal.TUNER_TYPE_NETWORK: + case Tuner.TUNER_TYPE_NETWORK: title = getString(R.string.nt_setup_new_title); description = getString(R.string.nt_setup_new_description); break; @@ -87,10 +87,10 @@ public class WelcomeFragment extends SetupMultiPaneFragment { } else { title = getString(R.string.bt_setup_again_title); switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: + case Tuner.TUNER_TYPE_USB: description = getString(R.string.ut_setup_again_description); break; - case TunerHal.TUNER_TYPE_NETWORK: + case Tuner.TUNER_TYPE_NETWORK: description = getString(R.string.nt_setup_again_description); break; default: diff --git a/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java new file mode 100644 index 00000000..48b17dcb --- /dev/null +++ b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 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.tuner.singletons; + +import com.android.tv.common.singletons.HasTvInputId; + +/** Singletons used in tuner applications */ +public interface TunerSingletons extends HasTvInputId {} diff --git a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java index ab05aa02..85932c8c 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java +++ b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.tvinput; +package com.android.tv.tuner.source; import android.util.Log; import android.util.SparseArray; @@ -27,9 +27,8 @@ import com.android.tv.tuner.data.PsipData.VctItem; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.source.FileTsStreamer; +import com.android.tv.tuner.ts.EventDetector.EventListener; import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -57,7 +56,7 @@ public class FileSourceEventDetector { private FileTsStreamer.StreamProvider mStreamProvider; private int mProgramNumber = ALL_PROGRAM_NUMBERS; - public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) { + public FileSourceEventDetector(EventListener listener, boolean enableDvbSignal) { mEventListener = listener; mEnableDvbSignal = enableDvbSignal; } diff --git a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java index 38a59b3d..99d37e39 100644 --- a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java +++ b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java @@ -21,12 +21,11 @@ import android.os.Environment; import android.util.Log; import android.util.SparseBooleanArray; import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.ChannelScanFileParser.ScanChannel; -import com.android.tv.tuner.TunerFeatures; +import com.android.tv.tuner.api.ScanChannel; import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.features.TunerFeatures; +import com.android.tv.tuner.ts.EventDetector.EventListener; import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.FileSourceEventDetector; import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSpec; import java.io.BufferedInputStream; @@ -125,7 +124,7 @@ public class FileTsStreamer implements TsStreamer { * * @param eventListener the listener for channel & program information */ - public FileTsStreamer(EventDetector.EventListener eventListener, Context context) { + public FileTsStreamer(EventListener eventListener, Context context) { mEventDetector = new FileSourceEventDetector( eventListener, TunerFeatures.ENABLE_FILE_DVB.isEnabled(context)); diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSource.java b/tuner/src/com/android/tv/tuner/source/TsDataSource.java index be902944..cf3c25d9 100644 --- a/tuner/src/com/android/tv/tuner/source/TsDataSource.java +++ b/tuner/src/com/android/tv/tuner/source/TsDataSource.java @@ -16,6 +16,7 @@ package com.android.tv.tuner.source; +import com.android.tv.common.compat.TvInputConstantCompat; import com.google.android.exoplayer.upstream.DataSource; /** {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */ @@ -46,4 +47,8 @@ public abstract class TsDataSource implements DataSource { * @param offset 0 <= offset <= buffered position */ public void shiftStartPosition(long offset) {} + + public int getSignalStrength() { + return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED; + } } diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java index 08acbc88..28756a93 100644 --- a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java +++ b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java @@ -18,49 +18,58 @@ package com.android.tv.tuner.source; import android.content.Context; import android.support.annotation.VisibleForTesting; -import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.api.Tuner; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.data.nano.Channel; -import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.ts.EventDetector.EventListener; +import com.google.auto.factory.AutoFactory; +import com.google.auto.factory.Provided; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import javax.inject.Inject; +import javax.inject.Provider; /** - * Manages {@link DataSource} for playback and recording. The class hides handling of {@link - * TunerHal} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created - * for per session. + * Manages {@link TsDataSource} for playback and recording. The class hides handling of {@link + * Tuner} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created for + * per session. */ +@AutoFactory public class TsDataSourceManager { - private static final Object sLock = new Object(); private static final Map<TsDataSource, TsStreamer> sTsStreamers = new ConcurrentHashMap<>(); - private static int sSequenceId; + private static final AtomicInteger sSequenceId = new AtomicInteger(); - private final int mId; + private final int mId = sSequenceId.incrementAndGet(); private final boolean mIsRecording; - private final TunerTsStreamerManager mTunerStreamerManager = - TunerTsStreamerManager.getInstance(); + private final TunerTsStreamerManager mTunerStreamerManager; private boolean mKeepTuneStatus; /** - * Creates TsDataSourceManager to create and release {@link DataSource} which will be used for - * playing and recording. + * Factory for {@link }TsDataSourceManager}. * - * @param isRecording {@code true} when for recording, {@code false} otherwise - * @return {@link TsDataSourceManager} + * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory} + * generated class. */ - public static TsDataSourceManager createSourceManager(boolean isRecording) { - int id; - synchronized (sLock) { - id = ++sSequenceId; + public static final class Factory { + private final TsDataSourceManagerFactory mDelegate; + + @Inject + public Factory(Provider<TunerTsStreamerManager> tunerStreamerManagerProvider) { + mDelegate = new TsDataSourceManagerFactory(tunerStreamerManagerProvider); + } + + public TsDataSourceManager create(boolean isRecording) { + return mDelegate.create(isRecording); } - return new TsDataSourceManager(id, isRecording); } - private TsDataSourceManager(int id, boolean isRecording) { - mId = id; + TsDataSourceManager( + boolean isRecording, @Provided TunerTsStreamerManager tunerStreamerManager) { mIsRecording = isRecording; + this.mTunerStreamerManager = tunerStreamerManager; mKeepTuneStatus = true; } @@ -73,7 +82,7 @@ public class TsDataSourceManager { * @return {@link TsDataSource} which will provide the specified channel stream */ public TsDataSource createDataSource( - Context context, TunerChannel channel, EventDetector.EventListener eventListener) { + Context context, TunerChannel channel, EventListener eventListener) { if (channel.getType() == Channel.TunerType.TYPE_FILE) { // MPEG2 TS captured stream file recording is not supported. if (mIsRecording) { @@ -92,7 +101,7 @@ public class TsDataSourceManager { } /** - * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}. + * Releases the specified {@link TsDataSource} and underlying {@link Tuner}. * * @param source to release */ @@ -114,10 +123,10 @@ public class TsDataSourceManager { } /** - * Indicates whether the underlying {@link TunerHal} should be kept or not when data source is + * Indicates whether the underlying {@link Tuner} should be kept or not when data source is * being released. TODO: If b/30750953 is fixed, we can remove this function. * - * @param keepTuneStatus underlying {@link TunerHal} will be reused when data source releasing. + * @param keepTuneStatus underlying {@link Tuner} will be reused when data source releasing. */ public void setKeepTuneStatus(boolean keepTuneStatus) { mKeepTuneStatus = keepTuneStatus; @@ -125,7 +134,7 @@ public class TsDataSourceManager { /** Add tuner hal into TunerTsStreamerManager for test. */ @VisibleForTesting - public void addTunerHalForTest(TunerHal tunerHal) { + public void addTunerHalForTest(Tuner tunerHal) { mTunerStreamerManager.addTunerHal(tunerHal, mId); } diff --git a/tuner/src/com/android/tv/tuner/source/TsStreamer.java b/tuner/src/com/android/tv/tuner/source/TsStreamer.java index 3dbba7e7..e5658e71 100644 --- a/tuner/src/com/android/tv/tuner/source/TsStreamer.java +++ b/tuner/src/com/android/tv/tuner/source/TsStreamer.java @@ -16,7 +16,7 @@ package com.android.tv.tuner.source; -import com.android.tv.tuner.ChannelScanFileParser; +import com.android.tv.tuner.api.ScanChannel; import com.android.tv.tuner.data.TunerChannel; /** @@ -27,10 +27,10 @@ public interface TsStreamer { /** * Starts streaming the data for channel scanning process. * - * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned + * @param channel {@link ScanChannel} to be scanned * @return {@code true} if ready to stream, otherwise {@code false} */ - boolean startStream(ChannelScanFileParser.ScanChannel channel); + boolean startStream(ScanChannel channel); /** * Starts streaming the data for channel playing or recording. diff --git a/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java new file mode 100644 index 00000000..12d2de1b --- /dev/null +++ b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.tuner.source; + +import com.android.tv.tuner.api.TunerFactory; +import dagger.Module; +import dagger.Provides; +import javax.inject.Singleton; + +/** Dagger module for TV Tuners Sources. */ +@Module() +public class TunerSourceModule { + @Provides + @Singleton + TunerTsStreamerManager providesTunerTsStreamerManager(TunerFactory tunerFactory) { + return new TunerTsStreamerManager(tunerFactory); + } +} diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java index 21b7a1f8..9e68c910 100644 --- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java @@ -20,12 +20,12 @@ import android.content.Context; import android.util.Log; import android.util.Pair; import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; +import com.android.tv.tuner.api.ScanChannel; +import com.android.tv.tuner.api.Tuner; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; +import com.android.tv.tuner.prefs.TunerPreferences; +import com.android.tv.tuner.ts.EventDetector; +import com.android.tv.tuner.ts.EventDetector.EventListener; import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSpec; import java.io.IOException; @@ -53,7 +53,7 @@ public class TunerTsStreamer implements TsStreamer { private final AtomicLong mLastReadPosition = new AtomicLong(); private boolean mStreaming; - private final TunerHal mTunerHal; + private final Tuner mTunerHal; private TunerChannel mChannel; private Thread mStreamingThread; private final EventDetector mEventDetector; @@ -121,6 +121,11 @@ public class TunerTsStreamer implements TsStreamer { } return ret; } + + @Override + public int getSignalStrength() { + return mTsStreamer.getSignalStrength(); + } } /** * Creates {@link TsStreamer} for playing or recording the specified channel. @@ -128,7 +133,7 @@ public class TunerTsStreamer implements TsStreamer { * @param tunerHal the HAL for tuner device * @param eventListener the listener for channel & program information */ - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { + public TunerTsStreamer(Tuner tunerHal, EventListener eventListener, Context context) { mTunerHal = tunerHal; mEventDetector = new EventDetector(mTunerHal); if (eventListener != null) { @@ -140,7 +145,7 @@ public class TunerTsStreamer implements TsStreamer { : null; } - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { + public TunerTsStreamer(Tuner tunerHal, EventListener eventListener) { this(tunerHal, eventListener, null); } @@ -149,20 +154,20 @@ public class TunerTsStreamer implements TsStreamer { if (mTunerHal.tune( channel.getFrequency(), channel.getModulation(), channel.getDisplayNumber(false))) { if (channel.hasVideo()) { - mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO); + mTunerHal.addPidFilter(channel.getVideoPid(), Tuner.FILTER_TYPE_VIDEO); } boolean audioFilterSet = false; for (Integer audioPid : channel.getAudioPids()) { if (!audioFilterSet) { - mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO); + mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_AUDIO); audioFilterSet = true; } else { // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks. - mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER); + mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_OTHER); } } - mTunerHal.addPidFilter(channel.getPcrPid(), TunerHal.FILTER_TYPE_PCR); + mTunerHal.addPidFilter(channel.getPcrPid(), Tuner.FILTER_TYPE_PCR); if (mEventDetector != null) { mEventDetector.startDetecting( channel.getFrequency(), @@ -193,7 +198,7 @@ public class TunerTsStreamer implements TsStreamer { } @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { + public boolean startStream(ScanChannel channel) { if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { mEventDetector.startDetecting( channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); @@ -255,11 +260,11 @@ public class TunerTsStreamer implements TsStreamer { } /** - * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer. + * Returns the current {@link Tuner} which provides MPEG-TS stream for TunerTsStreamer. * - * @return {@link TunerHal} + * @return {@link Tuner} */ - public TunerHal getTunerHal() { + public Tuner getTunerHal() { return mTunerHal; } @@ -303,6 +308,10 @@ public class TunerTsStreamer implements TsStreamer { } } + public int getSignalStrength() { + return mTunerHal.getSignalStrength(); + } + private class StreamingThread extends Thread { @Override public void run() { diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java index 44fb41e6..076206c4 100644 --- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java @@ -17,52 +17,49 @@ package com.android.tv.tuner.source; import android.content.Context; +import android.support.annotation.VisibleForTesting; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.util.AutoCloseableUtils; -import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.api.TunerFactory; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.ts.EventDetector.EventListener; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; /** * Manages {@link TunerTsStreamer} for playback and recording. The class hides handling of {@link - * TunerHal} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this + * Tuner} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this * class directly. */ -class TunerTsStreamerManager { +@Singleton +@VisibleForTesting +public class TunerTsStreamerManager { // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from // the same session. private final Object mCancelLock = new Object(); private final StreamerFinder mStreamerFinder = new StreamerFinder(); private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>(); - private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>(); + private final Map<Integer, EventListener> mListeners = new HashMap<>(); private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); - private final TunerHalManager mTunerHalManager = new TunerHalManager(); - private static TunerTsStreamerManager sInstance; + private final TunerHalManager mTunerHalManager; - /** - * Returns the singleton instance for the class - * - * @return TunerTsStreamerManager - */ - static synchronized TunerTsStreamerManager getInstance() { - if (sInstance == null) { - sInstance = new TunerTsStreamerManager(); - } - return sInstance; + @Inject + @VisibleForTesting + public TunerTsStreamerManager(TunerFactory tunerFactory) { + mTunerHalManager = new TunerHalManager(tunerFactory); } - private TunerTsStreamerManager() {} - synchronized TsDataSource createDataSource( Context context, TunerChannel channel, - EventDetector.EventListener listener, + EventListener listener, int sessionId, boolean reuse) { TsStreamerCreator creator; @@ -95,7 +92,7 @@ class TunerTsStreamerManager { } // Created streamer was cancelled by a new tune request. streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); + Tuner hal = streamer.getTunerHal(); hal.setHasPendingTune(false); mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); return null; @@ -109,7 +106,7 @@ class TunerTsStreamerManager { if (streamer == null) { return; } - EventDetector.EventListener listener = mListeners.remove(sessionId); + EventListener listener = mListeners.remove(sessionId); streamer.unregisterListener(listener); TunerChannel channel = streamer.getChannel(); SoftPreconditions.checkState(channel != null); @@ -119,7 +116,7 @@ class TunerTsStreamerManager { } } streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); + Tuner hal = streamer.getTunerHal(); hal.setHasPendingTune(false); mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); } @@ -133,7 +130,7 @@ class TunerTsStreamerManager { } /** Add tuner hal into TunerHalManager for test. */ - void addTunerHal(TunerHal tunerHal, int sessionId) { + void addTunerHal(Tuner tunerHal, int sessionId) { mTunerHalManager.addTunerHal(tunerHal, sessionId); } @@ -188,21 +185,20 @@ class TunerTsStreamerManager { private class TsStreamerCreator { private final Context mContext; private final TunerChannel mChannel; - private final EventDetector.EventListener mEventListener; + private final EventListener mEventListener; // mCancelled will be {@code true} if a new tune request for the same session // cancels create(). private boolean mCancelled; - private TunerHal mTunerHal; + private Tuner mTunerHal; - private TsStreamerCreator( - Context context, TunerChannel channel, EventDetector.EventListener listener) { + private TsStreamerCreator(Context context, TunerChannel channel, EventListener listener) { mContext = context; mChannel = channel; mEventListener = listener; } private TunerTsStreamer create(int sessionId, boolean reuse) { - TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId); + Tuner hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId); if (hal == null) { return null; } @@ -248,15 +244,20 @@ class TunerTsStreamerManager { } /** - * Supports sharing {@link TunerHal} among multiple sessions. The class also supports session - * affinity for {@link TunerHal} allocation. + * Supports sharing {@link Tuner} among multiple sessions. The class also supports session + * affinity for {@link Tuner} allocation. */ private static class TunerHalManager { - private final Map<Integer, TunerHal> mTunerHals = new HashMap<>(); + private final Map<Integer, Tuner> mTunerHals = new HashMap<>(); + private final TunerFactory mTunerFactory; + + private TunerHalManager(TunerFactory mTunerFactory) { + this.mTunerFactory = mTunerFactory; + } - private TunerHal getOrCreateTunerHal(Context context, int sessionId) { + private Tuner getOrCreateTunerHal(Context context, int sessionId) { // Handles session affinity. - TunerHal hal = mTunerHals.get(sessionId); + Tuner hal = mTunerHals.get(sessionId); if (hal != null) { mTunerHals.remove(sessionId); return hal; @@ -269,15 +270,15 @@ class TunerTsStreamerManager { mTunerHals.remove(key); return hal; } - return TunerHal.createInstance(context); + return mTunerFactory.createInstance(context); } - private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { + private void releaseTunerHal(Tuner hal, int sessionId, boolean reuse) { if (!reuse || !hal.isReusable()) { AutoCloseableUtils.closeQuietly(hal); return; } - TunerHal cachedHal = mTunerHals.get(sessionId); + Tuner cachedHal = mTunerHals.get(sessionId); if (cachedHal != hal) { mTunerHals.put(sessionId, hal); if (cachedHal != null) { @@ -287,7 +288,7 @@ class TunerTsStreamerManager { } private void releaseCachedHal(int sessionId) { - TunerHal hal = mTunerHals.get(sessionId); + Tuner hal = mTunerHals.get(sessionId); if (hal != null) { mTunerHals.remove(sessionId); } @@ -296,7 +297,7 @@ class TunerTsStreamerManager { } } - private void addTunerHal(TunerHal tunerHal, int sessionId) { + private void addTunerHal(Tuner tunerHal, int sessionId) { mTunerHals.put(sessionId, tunerHal); } } diff --git a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java b/tuner/src/com/android/tv/tuner/ts/EventDetector.java index c529c6db..6d1fc277 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java +++ b/tuner/src/com/android/tv/tuner/ts/EventDetector.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package com.android.tv.tuner.tvinput; +package com.android.tv.tuner.ts; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; -import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.api.Tuner; import com.android.tv.tuner.data.PsiData; import com.android.tv.tuner.data.PsipData; +import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.ts.TsParser; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -39,7 +39,7 @@ public class EventDetector { private static final boolean DEBUG = false; public static final int ALL_PROGRAM_NUMBERS = -1; - private final TunerHal mTunerHal; + private final Tuner mTunerHal; private TsParser mTsParser; private final Set<Integer> mPidSet = new HashSet<>(); @@ -62,7 +62,7 @@ public class EventDetector { for (PsiData.PatItem i : items) { if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { - mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER); + mTunerHal.addPidFilter(i.getPmtPid(), Tuner.FILTER_TYPE_OTHER); } } } @@ -225,15 +225,7 @@ public class EventDetector { }; /** Listener for detecting ATSC TV channels and receiving EPG data. */ - public interface EventListener { - - /** - * Fired when new information of an ATSC TV channel arrived. - * - * @param channel an ATSC TV channel - * @param channelArrivedAtFirstTime tells whether this channel arrived at first time - */ - void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime); + public interface EventListener extends com.android.tv.tuner.api.ChannelScanListener { /** * Fired when new program events of an ATSC TV channel arrived. @@ -241,7 +233,7 @@ public class EventDetector { * @param channel an ATSC TV channel * @param items a list of EIT items that were received */ - void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items); + void onEventDetected(TunerChannel channel, List<EitItem> items); /** * Fired when information of all detectable ATSC TV channels in current frequency arrived. @@ -250,21 +242,20 @@ public class EventDetector { } /** - * Creates a detector for ATSC TV channles and program information. + * Creates a detector for ATSC TV channels and program information. * - * @param usbTunerInteface {@link TunerHal} + * @param tunerHal */ - public EventDetector(TunerHal usbTunerInteface) { - mTunerHal = usbTunerInteface; + public EventDetector(Tuner tunerHal) { + mTunerHal = tunerHal; } private void reset() { // TODO: Use TsParser.reset() - int deliverySystemType = mTunerHal.getDeliverySystemType(); mTsParser = new TsParser( mTsOutputListener, - TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType())); + Tuner.isDvbDeliverySystem(mTunerHal.getDeliverySystemType())); mPidSet.clear(); mVctProgramNumberSet.clear(); mSdtProgramNumberSet.clear(); @@ -293,7 +284,7 @@ public class EventDetector { return; } mPidSet.add(pid); - mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER); + mTunerHal.addPidFilter(pid, Tuner.FILTER_TYPE_OTHER); } /** diff --git a/tuner/src/com/android/tv/tuner/ts/TsParser.java b/tuner/src/com/android/tv/tuner/ts/TsParser.java index 2307c22a..be46983b 100644 --- a/tuner/src/com/android/tv/tuner/ts/TsParser.java +++ b/tuner/src/com/android/tv/tuner/ts/TsParser.java @@ -26,8 +26,9 @@ import com.android.tv.tuner.data.PsipData.EttItem; import com.android.tv.tuner.data.PsipData.MgtItem; import com.android.tv.tuner.data.PsipData.SdtItem; import com.android.tv.tuner.data.PsipData.VctItem; +import com.android.tv.tuner.data.SectionParser; +import com.android.tv.tuner.data.SectionParser.OutputListener; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.ts.SectionParser.OutputListener; import com.android.tv.tuner.util.ByteArrayBuffer; import java.util.ArrayList; import java.util.Arrays; diff --git a/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java new file mode 100644 index 00000000..4c35ea43 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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.tuner.tvinput; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; + +/** + * Wraps {@link AudioCapabilitiesReceiver} to support listening for audio capabilities changes on + * custom threads. + */ +public final class AudioCapabilitiesReceiverV1Wrapper { + + private static final String TAG = "AudioCapabilitiesReceiverV1Wrapper"; + + private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; + private final Handler mHandler; + private final AudioCapabilitiesReceiver.Listener mListener; + private boolean mRegistered; + + /** + * Creates an instance. + * + * @param context A context for registering the receiver. + * @param handler A handler on the which mListener events will be posted. + * @param listener The listener to notify when audio capabilities change. + */ + public AudioCapabilitiesReceiverV1Wrapper( + Context context, Handler handler, AudioCapabilitiesReceiver.Listener listener) { + mAudioCapabilitiesReceiver = + new AudioCapabilitiesReceiver(context, this::onAudioCapabilitiesChanged); + mHandler = handler; + mListener = listener; + } + + /** @see AudioCapabilitiesReceiver#register() */ + public AudioCapabilities register() { + mRegistered = true; + return mAudioCapabilitiesReceiver.register(); + } + + /** @see AudioCapabilitiesReceiver#unregister() */ + public void unregister() { + if (mRegistered) { + try { + mAudioCapabilitiesReceiver.unregister(); + } catch (IllegalArgumentException e) { + // Workaround for b/115739362. + Log.e( + TAG, + "Ignoring exception when unregistering audio capabilities receiver: ", + e); + } + mRegistered = false; + } else { + Log.e(TAG, "Attempt to unregister a non-registered audio capabilities receiver."); + } + } + + private void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + mHandler.post(() -> mListener.onAudioCapabilitiesChanged(audioCapabilities)); + } +} diff --git a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java index e577e35e..d22b6399 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java +++ b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java @@ -23,26 +23,29 @@ import android.content.Context; import android.media.tv.TvInputService; import android.util.Log; import com.android.tv.common.feature.CommonFeatures; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory; +import dagger.android.AndroidInjection; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; /** {@link BaseTunerTvInputService} serves TV channels coming from a tuner device. */ -public class BaseTunerTvInputService extends TvInputService - implements AudioCapabilitiesReceiver.Listener { +public class BaseTunerTvInputService extends TvInputService { private static final String TAG = "BaseTunerTvInputService"; private static final boolean DEBUG = false; private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100; - // WeakContainer for {@link TvInputSessionImpl} - private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>()); + private final Set<Session> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>()); private ChannelDataManager mChannelDataManager; - private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; - private AudioCapabilities mAudioCapabilities; + @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + @Inject TsDataSourceManager.Factory mTsDataSourceManagerFactory; + @Inject TunerSessionFactory mTunerSessionFactory; @Override public void onCreate() { @@ -51,11 +54,10 @@ public class BaseTunerTvInputService extends TvInputService this.stopSelf(); return; } + AndroidInjection.inject(this); super.onCreate(); if (DEBUG) Log.d(TAG, "onCreate"); mChannelDataManager = new ChannelDataManager(getApplicationContext()); - mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); - mAudioCapabilitiesReceiver.register(); if (CommonFeatures.DVR.isEnabled(this)) { JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); @@ -80,12 +82,16 @@ public class BaseTunerTvInputService extends TvInputService if (DEBUG) Log.d(TAG, "onDestroy"); super.onDestroy(); mChannelDataManager.release(); - mAudioCapabilitiesReceiver.unregister(); } @Override public RecordingSession onCreateRecordingSession(String inputId) { - return new TunerRecordingSession(this, inputId, mChannelDataManager); + return new TunerRecordingSession( + this, + inputId, + mChannelDataManager, + mConcurrentDvrPlaybackFlags, + mTsDataSourceManagerFactory); } @Override @@ -93,13 +99,13 @@ public class BaseTunerTvInputService extends TvInputService if (DEBUG) Log.d(TAG, "onCreateSession"); try { // TODO(b/65445352): Support multiple TunerSessions for multiple tuners - if (!allSessionsReleased()) { + if (!mTunerSessions.isEmpty()) { Log.d(TAG, "abort creating an session"); return null; } - final TunerSession session = new TunerSession(this, mChannelDataManager); + final Session session = + mTunerSessionFactory.create(this, mChannelDataManager, this::onReleased); mTunerSessions.add(session); - session.setAudioCapabilities(mAudioCapabilities); session.setOverlayViewEnabled(true); return session; } catch (RuntimeException e) { @@ -109,22 +115,7 @@ public class BaseTunerTvInputService extends TvInputService } } - @Override - public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - mAudioCapabilities = audioCapabilities; - for (TunerSession session : mTunerSessions) { - if (!session.isReleased()) { - session.setAudioCapabilities(audioCapabilities); - } - } - } - - private boolean allSessionsReleased() { - for (TunerSession session : mTunerSessions) { - if (!session.isReleased()) { - return false; - } - } - return true; + private void onReleased(Session session) { + mTunerSessions.remove(session); } } diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java index a1f0c773..55616931 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java @@ -17,25 +17,38 @@ package com.android.tv.tuner.tvinput; import android.content.Context; -import android.media.tv.TvInputService; import android.net.Uri; import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.util.Log; +import com.android.tv.common.compat.RecordingSessionCompat; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; /** Processes DVR recordings, and deletes the previously recorded contents. */ -public class TunerRecordingSession extends TvInputService.RecordingSession { +public class TunerRecordingSession extends RecordingSessionCompat { private static final String TAG = "TunerRecordingSession"; private static final boolean DEBUG = false; private final TunerRecordingSessionWorker mSessionWorker; public TunerRecordingSession( - Context context, String inputId, ChannelDataManager channelDataManager) { + Context context, + String inputId, + ChannelDataManager channelDataManager, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { super(context); mSessionWorker = - new TunerRecordingSessionWorker(context, inputId, channelDataManager, this); + new TunerRecordingSessionWorker( + context, + inputId, + channelDataManager, + this, + concurrentDvrPlaybackFlags, + tsDataSourceManagerFactory); } // RecordingSession @@ -85,6 +98,15 @@ public class TunerRecordingSession extends TvInputService.RecordingSession { notifyTuned(channelUri); } + // Called from TunerRecordingSessionImpl in a worker thread. + @WorkerThread + public void onRecordingUri(String recUri) { + if (DEBUG) { + Log.d(TAG, "Notifying recording session URI." + recUri); + } + notifyRecordingStarted(recUri); + } + @WorkerThread public void onRecordFinished(final Uri recordedProgramUri) { if (DEBUG) { diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java index b2001225..2c0c09a6 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java @@ -16,6 +16,8 @@ package com.android.tv.tuner.tvinput; +import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION; + import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -26,16 +28,18 @@ import android.media.tv.TvContract.RecordedPrograms; import android.media.tv.TvInputManager; import android.net.Uri; import android.os.AsyncTask; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.MainThread; import android.support.annotation.Nullable; -import android.support.media.tv.Program; import android.util.Log; import android.util.Pair; +import androidx.tvprovider.media.tv.Program; import com.android.tv.common.BaseApplication; +import com.android.tv.common.data.RecordedProgramState; import com.android.tv.common.recording.RecordingCapability; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.util.CommonUtils; @@ -48,23 +52,31 @@ import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; import com.android.tv.tuner.exoplayer.SampleExtractor; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.ts.EventDetector.EventListener; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; import com.google.android.exoplayer.C; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.Set; import java.util.concurrent.TimeUnit; /** Implements a DVR feature. */ public class TunerRecordingSessionWorker implements PlaybackBufferListener, - EventDetector.EventListener, + EventListener, SampleExtractor.OnCompletionListener, Handler.Callback { private static final String TAG = "TunerRecordingSessionW"; @@ -87,6 +99,14 @@ public class TunerRecordingSessionWorker private static final int MSG_MONITOR_STORAGE_STATUS = 5; private static final int MSG_RELEASE = 6; private static final int MSG_UPDATE_CC_INFO = 7; + private static final int MSG_UPDATE_PARTIAL_STATE = 8; + private static final String COLUMN_SERIES_ID = "series_id"; + private static final String COLUMN_STATE = "state"; + + private boolean mProgramHasSeriesIdColumn; + private boolean mRecordedProgramHasSeriesIdColumn; + private boolean mRecordedProgramHasStateColumn; + private final RecordingCapability mCapabilities; private static final String[] PROGRAM_PROJECTION = { @@ -108,6 +128,9 @@ public class TunerRecordingSessionWorker TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA }; + private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID = + createProjectionWithSeriesId(); + @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) @Retention(RetentionPolicy.SOURCE) public @interface DvrSessionState {} @@ -119,6 +142,7 @@ public class TunerRecordingSessionWorker private static final long CHANNEL_ID_NONE = -1; private static final int MAX_TUNING_RETRY = 6; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; private final Context mContext; private final ChannelDataManager mChannelDataManager; @@ -132,12 +156,14 @@ public class TunerRecordingSessionWorker private File mStorageDir; private long mRecordStartTime; private long mRecordEndTime; + private Uri mRecordedProgramUri; private boolean mRecorderRunning; private SampleExtractor mRecorder; private final TunerRecordingSession mSession; @DvrSessionState private int mSessionState = STATE_IDLE; private final String mInputId; private Uri mProgramUri; + private String mSeriesId; private PsipData.EitItem mCurrenProgram; private List<AtscCaptionTrack> mCaptionTracks; @@ -147,7 +173,10 @@ public class TunerRecordingSessionWorker Context context, String inputId, ChannelDataManager dataManager, - TunerRecordingSession session) { + TunerRecordingSession session, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; mRandom.setSeed(System.nanoTime()); mContext = context; HandlerThread handlerThread = new HandlerThread(TAG); @@ -157,7 +186,7 @@ public class TunerRecordingSessionWorker BaseApplication.getSingletons(context).getRecordingStorageStatusManager(); mChannelDataManager = dataManager; mChannelDataManager.checkDataVersion(context); - mSourceManager = TsDataSourceManager.createSourceManager(true); + mSourceManager = tsDataSourceManagerFactory.create(true); mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); mInputId = inputId; if (DEBUG) Log.d(TAG, mCapabilities.toString()); @@ -306,6 +335,7 @@ public class TunerRecordingSessionWorker } new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + mContext.getContentResolver().delete(mRecordedProgramUri, null, null); reset(); } else { mHandler.sendEmptyMessageDelayed( @@ -330,6 +360,11 @@ public class TunerRecordingSessionWorker updateCaptionTracks(pair.first, pair.second); return true; } + case MSG_UPDATE_PARTIAL_STATE: + { + updateRecordedProgram(RecordedProgramState.PARTIAL, -1, -1); + return true; + } } return false; } @@ -422,17 +457,46 @@ public class TunerRecordingSessionWorker mDvrStorageManager = new DvrStorageManager(mStorageDir, true); mRecorder = new ExoPlayerSampleExtractor( - Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true); + Uri.EMPTY, + mTunerSource, + new BufferManager(mDvrStorageManager), + this, + true, + mConcurrentDvrPlaybackFlags); mRecorder.setOnCompletionListener(this, mHandler); mProgramUri = programUri; mSessionState = STATE_RECORDING; mRecorderRunning = true; + if (mConcurrentDvrPlaybackFlags.enabled()) { + mRecordedProgramUri = + insertRecordedProgram( + getRecordedProgram(), + mChannel.getChannelId(), + Uri.fromFile(mStorageDir).toString(), + calculateRecordingSizeInBytes(), + mRecordStartTime, + mRecordStartTime); + if (mRecordedProgramUri == null) { + new DeleteRecordingTask().execute(mStorageDir); + mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); + Log.e(TAG, "Inserting a recording to DB failed"); + return false; + } + mSession.onRecordingUri(mRecordedProgramUri.toString()); + mHandler.sendEmptyMessageDelayed( + MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS); + } mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS); return true; } + private int calculateRecordingSizeInBytes() { + // TODO(b/121153491): calcute recording size using mStorageDir + return 1024 * 1024; + } + private void stopRecorder() { // Do not change session status. if (mRecorder != null) { @@ -485,9 +549,15 @@ public class TunerRecordingSessionWorker long avg = mRecordStartTime / 2 + mRecordEndTime / 2; programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); } - try (Cursor c = resolver.query(programUri, PROGRAM_PROJECTION, null, null, SORT_BY_TIME)) { + String[] projection = + checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION; + try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) { if (c != null && c.moveToNext()) { Program result = Program.fromCursor(c); + int index; + if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) { + mSeriesId = c.getString(index); + } if (DEBUG) { Log.v(TAG, "Finished query for " + this); } @@ -516,9 +586,15 @@ public class TunerRecordingSessionWorker values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri); values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime); values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes); - // startTime and endTime could be overridden by program's start and end value. + // startTime could be overridden by program's start value. values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime); values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); + if (checkRecordedProgramTable(COLUMN_SERIES_ID)) { + values.put(COLUMN_SERIES_ID, mSeriesId); + } + if (mConcurrentDvrPlaybackFlags.enabled() && checkRecordedProgramTable(COLUMN_STATE)) { + values.put(COLUMN_STATE, RecordedProgramState.STARTED.name()); + } if (program != null) { values.putAll(program.toContentValues()); } @@ -526,6 +602,20 @@ public class TunerRecordingSessionWorker .insert(TvContract.RecordedPrograms.CONTENT_URI, values); } + private void updateRecordedProgram(RecordedProgramState state, long endTime, long totalBytes) { + ContentValues values = new ContentValues(); + if (checkRecordedProgramTable(COLUMN_STATE)) { + values.put(COLUMN_STATE, state.name()); + } + if (state.equals(RecordedProgramState.FINISHED)) { + values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes); + values.put( + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime); + values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime); + } + mContext.getContentResolver().update(mRecordedProgramUri, values, null, null); + } + private void onRecordingResult(boolean success, long lastExtractedPositionUs) { if (mSessionState != STATE_RECORDING) { // Error notification is not needed. @@ -541,6 +631,7 @@ public class TunerRecordingSessionWorker < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); + mContext.getContentResolver().delete(mRecordedProgramUri, null, null); Log.w(TAG, "Recording failed during recording"); return; } @@ -549,22 +640,120 @@ public class TunerRecordingSessionWorker (lastExtractedPositionUs == C.UNKNOWN_TIME_US) ? System.currentTimeMillis() : mRecordStartTime + lastExtractedPositionUs / 1000; - Uri uri = - insertRecordedProgram( - getRecordedProgram(), - mChannel.getChannelId(), - Uri.fromFile(mStorageDir).toString(), - 1024 * 1024, - mRecordStartTime, - recordEndTime); - if (uri == null) { - new DeleteRecordingTask().execute(mStorageDir); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.e(TAG, "Inserting a recording to DB failed"); - return; + if (!mConcurrentDvrPlaybackFlags.enabled()) { + mRecordedProgramUri = + insertRecordedProgram( + getRecordedProgram(), + mChannel.getChannelId(), + Uri.fromFile(mStorageDir).toString(), + calculateRecordingSizeInBytes(), + mRecordStartTime, + recordEndTime); + if (mRecordedProgramUri == null) { + new DeleteRecordingTask().execute(mStorageDir); + mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); + Log.e(TAG, "Inserting a recording to DB failed"); + return; + } + } else { + updateRecordedProgram( + RecordedProgramState.FINISHED, recordEndTime, calculateRecordingSizeInBytes()); } mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); - mSession.onRecordFinished(uri); + mSession.onRecordFinished(mRecordedProgramUri); + } + + private boolean checkProgramTable() { + boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext); + if (!canCreateColumn) { + return false; + } + Uri uri = TvContract.Programs.CONTENT_URI; + if (!mProgramHasSeriesIdColumn) { + if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) { + mProgramHasSeriesIdColumn = true; + } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) { + mProgramHasSeriesIdColumn = true; + } + } + return mProgramHasSeriesIdColumn; + } + + private boolean checkRecordedProgramTable(String column) { + boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext); + if (!canCreateColumn) { + return false; + } + Uri uri = TvContract.RecordedPrograms.CONTENT_URI; + switch (column) { + case COLUMN_SERIES_ID: + { + if (!mRecordedProgramHasSeriesIdColumn) { + if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) { + mRecordedProgramHasSeriesIdColumn = true; + } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) { + mRecordedProgramHasSeriesIdColumn = true; + } + } + return mRecordedProgramHasSeriesIdColumn; + } + case COLUMN_STATE: + { + if (!mRecordedProgramHasStateColumn) { + if (getExistingColumns(uri).contains(COLUMN_STATE)) { + mRecordedProgramHasStateColumn = true; + } else if (addColumnToTable(uri, COLUMN_STATE)) { + mRecordedProgramHasStateColumn = true; + } + } + return mRecordedProgramHasStateColumn; + } + default: + return false; + } + } + + private Set<String> getExistingColumns(Uri uri) { + Bundle result = + mContext.getContentResolver() + .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); + if (result != null) { + String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); + if (columns != null) { + return new HashSet<>(Arrays.asList(columns)); + } + } + Log.e(TAG, "Query existing column names from " + uri + " returned null"); + return Collections.emptySet(); + } + + /** + * Add a column to the table + * + * @return {@code true} if the column is added successfully; {@code false} otherwise. + */ + private boolean addColumnToTable(Uri contentUri, String columnName) { + Bundle extra = new Bundle(); + extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); + extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); + // If the add operation fails, the following just returns null without crashing. + Bundle allColumns = + mContext.getContentResolver() + .call( + contentUri, + TvContract.METHOD_ADD_COLUMN, + contentUri.toString(), + extra); + if (allColumns == null) { + Log.w(TAG, "Adding new column failed. Uri=" + contentUri); + } + return allColumns != null; + } + + private static String[] createProjectionWithSeriesId() { + List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION)); + projectionList.add(COLUMN_SERIES_ID); + return projectionList.toArray(new String[0]); } private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { @@ -575,7 +764,9 @@ public class TunerRecordingSessionWorker return null; } for (File file : files) { - CommonUtils.deleteDirOrFile(file); + if (!CommonUtils.deleteDirOrFile(file)) { + Log.w(TAG, "Unable to delete recording data at " + file); + } } return null; } diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java index c9d997f1..fedb5f6b 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java @@ -21,95 +21,58 @@ import android.content.Context; import android.media.PlaybackParams; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; -import android.media.tv.TvInputService; import android.net.Uri; import android.os.Build; -import android.os.Handler; -import android.os.Message; import android.os.SystemClock; -import android.text.Html; import android.util.Log; -import android.view.LayoutInflater; import android.view.Surface; import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener; -import com.android.tv.common.util.SystemPropertiesProxy; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.cc.CaptionLayout; -import com.android.tv.tuner.cc.CaptionTrackRenderer; -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.util.GlobalSettingsUtils; -import com.android.tv.tuner.util.StatusTextUtils; -import com.google.android.exoplayer.audio.AudioCapabilities; +import com.android.tv.common.compat.TisSessionCompat; +import com.android.tv.tuner.prefs.TunerPreferences; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; /** - * Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions are - * implemented in {@link TunerSessionWorker}. + * Provides a tuner TV input session. Main tuner input functions are implemented in {@link + * TunerSessionWorker}. */ -public class TunerSession extends TvInputService.Session - implements Handler.Callback, CommonPreferencesChangedListener { +public class TunerSession extends TisSessionCompat implements CommonPreferencesChangedListener { + private static final String TAG = "TunerSession"; private static final boolean DEBUG = false; - private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug"; - - public static final int MSG_UI_SHOW_MESSAGE = 1; - public static final int MSG_UI_HIDE_MESSAGE = 2; - public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3; - public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4; - public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5; - public static final int MSG_UI_START_CAPTION_TRACK = 6; - public static final int MSG_UI_STOP_CAPTION_TRACK = 7; - public static final int MSG_UI_RESET_CAPTION_TRACK = 8; - public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9; - public static final int MSG_UI_SET_STATUS_TEXT = 10; - public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11; - private final Context mContext; - private final Handler mUiHandler; - private final View mOverlayView; - private final TextView mMessageView; - private final TextView mStatusView; - private final TextView mAudioStatusView; - private final ViewGroup mMessageLayout; - private final CaptionTrackRenderer mCaptionTrackRenderer; + private final TunerSessionOverlay mTunerSessionOverlay; private final TunerSessionWorker mSessionWorker; - private boolean mReleased = false; + private final SessionReleasedCallback mReleasedCallback; private boolean mPlayPaused; private long mTuneStartTimestamp; - public TunerSession(Context context, ChannelDataManager channelDataManager) { + public TunerSession( + Context context, + ChannelDataManager channelDataManager, + SessionReleasedCallback releasedCallback, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { super(context); - mContext = context; - mUiHandler = new Handler(this); - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null); - mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout); - mMessageLayout.setVisibility(View.INVISIBLE); - mMessageView = (TextView) mOverlayView.findViewById(R.id.message); - mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status); - boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false); - mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE); - mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status); - mAudioStatusView.setVisibility(View.INVISIBLE); - CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); - mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); - mSessionWorker = new TunerSessionWorker(context, channelDataManager, this); + mReleasedCallback = releasedCallback; + mTunerSessionOverlay = new TunerSessionOverlay(context); + mSessionWorker = + new TunerSessionWorker( + context, + channelDataManager, + this, + mTunerSessionOverlay, + concurrentDvrPlaybackFlags, + tsDataSourceManagerFactory); TunerPreferences.setCommonPreferencesChangedListener(this); } - public boolean isReleased() { - return mReleased; - } - @Override public View onCreateOverlayView() { - return mOverlayView; + return mTunerSessionOverlay.getOverlayView(); } @Override @@ -207,16 +170,12 @@ public class TunerSession extends TvInputService.Session if (DEBUG) { Log.d(TAG, "onRelease"); } - mReleased = true; + // The session worker needs to be released before the overlay to ensure no messages are + // added by the worker after releasing the overlay. mSessionWorker.release(); - mUiHandler.removeCallbacksAndMessages(null); + mTunerSessionOverlay.release(); TunerPreferences.setCommonPreferencesChangedListener(null); - } - - /** Sets {@link AudioCapabilities}. */ - public void setAudioCapabilities(AudioCapabilities audioCapabilities) { - mSessionWorker.sendMessage( - TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, audioCapabilities); + mReleasedCallback.onReleased(this); } @Override @@ -241,99 +200,6 @@ public class TunerSession extends TvInputService.Session } } - public void sendUiMessage(int message) { - mUiHandler.sendEmptyMessage(message); - } - - public void sendUiMessage(int message, Object object) { - mUiHandler.obtainMessage(message, object).sendToTarget(); - } - - public void sendUiMessage(int message, int arg1, int arg2, Object object) { - mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget(); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UI_SHOW_MESSAGE: - { - mMessageView.setText((String) msg.obj); - mMessageLayout.setVisibility(View.VISIBLE); - return true; - } - case MSG_UI_HIDE_MESSAGE: - { - mMessageLayout.setVisibility(View.INVISIBLE); - return true; - } - case MSG_UI_SHOW_AUDIO_UNPLAYABLE: - { - // Showing message of enabling surround sound only when global surround sound - // setting is "never". - final int value = - GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); - if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { - mAudioStatusView.setText( - Html.fromHtml( - StatusTextUtils.getAudioWarningInHTML( - mContext.getString( - R.string.ut_surround_sound_disabled)))); - } else { - mAudioStatusView.setText( - Html.fromHtml( - StatusTextUtils.getAudioWarningInHTML( - mContext.getString( - R.string - .audio_passthrough_not_supported)))); - } - mAudioStatusView.setVisibility(View.VISIBLE); - return true; - } - case MSG_UI_HIDE_AUDIO_UNPLAYABLE: - { - mAudioStatusView.setVisibility(View.INVISIBLE); - return true; - } - case MSG_UI_PROCESS_CAPTION_TRACK: - { - mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj); - return true; - } - case MSG_UI_START_CAPTION_TRACK: - { - mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj); - return true; - } - case MSG_UI_STOP_CAPTION_TRACK: - { - mCaptionTrackRenderer.stop(); - return true; - } - case MSG_UI_RESET_CAPTION_TRACK: - { - mCaptionTrackRenderer.reset(); - return true; - } - case MSG_UI_CLEAR_CAPTION_RENDERER: - { - mCaptionTrackRenderer.clear(); - return true; - } - case MSG_UI_SET_STATUS_TEXT: - { - mStatusView.setText((CharSequence) msg.obj); - return true; - } - case MSG_UI_TOAST_RESCAN_NEEDED: - { - Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show(); - return true; - } - } - return false; - } - @Override public void onCommonPreferencesChanged() { mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED); diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java new file mode 100644 index 00000000..4eca44d6 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2015 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.tuner.tvinput; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.net.Uri; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; +import android.view.Surface; +import android.view.View; +import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener; +import com.android.tv.common.compat.TisSessionCompat; +import com.android.tv.tuner.prefs.TunerPreferences; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; + +/** Provides a tuner TV input session. */ +public class TunerSessionExoV2 extends TisSessionCompat + implements CommonPreferencesChangedListener { + + private static final String TAG = "TunerSessionExoV2"; + private static final boolean DEBUG = false; + + private final TunerSessionOverlay mTunerSessionOverlay; + private final TunerSessionWorkerExoV2 mSessionWorker; + private final SessionReleasedCallback mReleasedCallback; + private boolean mPlayPaused; + private long mTuneStartTimestamp; + + public TunerSessionExoV2( + Context context, + ChannelDataManager channelDataManager, + SessionReleasedCallback releasedCallback, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + super(context); + mReleasedCallback = releasedCallback; + mTunerSessionOverlay = new TunerSessionOverlay(context); + mSessionWorker = + new TunerSessionWorkerExoV2( + context, + channelDataManager, + this, + mTunerSessionOverlay, + concurrentDvrPlaybackFlags, + tsDataSourceManagerFactory); + TunerPreferences.setCommonPreferencesChangedListener(this); + } + + @Override + public View onCreateOverlayView() { + return mTunerSessionOverlay.getOverlayView(); + } + + @Override + public boolean onSelectTrack(int type, String trackId) { + mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_SELECT_TRACK, type, 0, trackId); + return false; + } + + @Override + public void onSetCaptionEnabled(boolean enabled) { + mSessionWorker.setCaptionEnabled(enabled); + } + + @Override + public void onSetStreamVolume(float volume) { + mSessionWorker.setStreamVolume(volume); + } + + @Override + public boolean onSetSurface(Surface surface) { + mSessionWorker.setSurface(surface); + return true; + } + + @Override + public void onTimeShiftPause() { + mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_PAUSE); + mPlayPaused = true; + } + + @Override + public void onTimeShiftResume() { + mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_RESUME); + mPlayPaused = false; + } + + @Override + public void onTimeShiftSeekTo(long timeMs) { + if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000); + mSessionWorker.sendMessage( + TunerSessionWorkerExoV2.MSG_TIMESHIFT_SEEK_TO, mPlayPaused ? 1 : 0, 0, timeMs); + } + + @Override + public void onTimeShiftSetPlaybackParams(PlaybackParams params) { + mSessionWorker.sendMessage( + TunerSessionWorkerExoV2.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params); + } + + @Override + public long onTimeShiftGetStartPosition() { + return mSessionWorker.getStartPosition(); + } + + @Override + public long onTimeShiftGetCurrentPosition() { + return mSessionWorker.getCurrentPosition(); + } + + @Override + public boolean onTune(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : ""); + } + if (channelUri == null) { + Log.w(TAG, "onTune() is failed due to null channelUri."); + mSessionWorker.stopTune(); + return false; + } + mTuneStartTimestamp = SystemClock.elapsedRealtime(); + mSessionWorker.tune(channelUri); + mPlayPaused = false; + return true; + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onTimeShiftPlay(Uri recordUri) { + if (recordUri == null) { + Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri."); + mSessionWorker.stopTune(); + return; + } + mTuneStartTimestamp = SystemClock.elapsedRealtime(); + mSessionWorker.tune(recordUri); + mPlayPaused = false; + } + + @Override + public void onUnblockContent(TvContentRating unblockedRating) { + mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_UNBLOCKED_RATING, unblockedRating); + } + + @Override + public void onRelease() { + if (DEBUG) { + Log.d(TAG, "onRelease"); + } + // The session worker needs to be released before the overlay to ensure no messages are + // added by the worker after releasing the overlay. + mSessionWorker.release(); + mTunerSessionOverlay.release(); + TunerPreferences.setCommonPreferencesChangedListener(null); + mReleasedCallback.onReleased(this); + } + + @Override + public void notifyVideoAvailable() { + super.notifyVideoAvailable(); + if (mTuneStartTimestamp != 0) { + Log.i( + TAG, + "[Profiler] Video available in " + + (SystemClock.elapsedRealtime() - mTuneStartTimestamp) + + " ms"); + mTuneStartTimestamp = 0; + } + } + + @Override + public void notifyVideoUnavailable(int reason) { + super.notifyVideoUnavailable(reason); + if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING + && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) { + notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); + } + } + + @Override + public void onCommonPreferencesChanged() { + mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TUNER_PREFERENCES_CHANGED); + } +} diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java new file mode 100644 index 00000000..9f21e16a --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2015 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.tuner.tvinput; + +import android.content.Context; +import android.media.tv.TvInputService.Session; +import android.os.Handler; +import android.os.Message; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import com.android.tv.common.util.SystemPropertiesProxy; +import com.android.tv.tuner.R; +import com.android.tv.tuner.cc.CaptionLayout; +import com.android.tv.tuner.cc.CaptionTrackRenderer; +import com.android.tv.tuner.data.Cea708Data.CaptionEvent; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; +import com.android.tv.tuner.util.GlobalSettingsUtils; +import com.android.tv.tuner.util.StatusTextUtils; + +/** Executes {@link Session} overlay changes on the main thread. */ +/* package */ final class TunerSessionOverlay implements Handler.Callback { + + /** Displays the given {@link String} message object in the message view. */ + public static final int MSG_UI_SHOW_MESSAGE = 1; + /** Hides the message view. Does not expect a message object. */ + public static final int MSG_UI_HIDE_MESSAGE = 2; + /** + * Displays a message in the audio status view to signal audio is not supported. Does not expect + * a message object. + */ + public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3; + /** Hides the audio status view. Does not expect a message object. */ + public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4; + /** Feeds the given {@link CaptionEvent} message object to the {@link CaptionTrackRenderer}. */ + public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5; + /** + * Invokes {@link CaptionTrackRenderer#start(AtscCaptionTrack)} passing the given {@link + * AtscCaptionTrack} message object as argument. + */ + public static final int MSG_UI_START_CAPTION_TRACK = 6; + /** Invokes {@link CaptionTrackRenderer#stop()}. Does not expect a message object. */ + public static final int MSG_UI_STOP_CAPTION_TRACK = 7; + /** Invokes {@link CaptionTrackRenderer#reset()}. Does not expect a message object. */ + public static final int MSG_UI_RESET_CAPTION_TRACK = 8; + /** Invokes {@link CaptionTrackRenderer#clear()}. Does not expect a message object. */ + public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9; + /** Displays the given {@link CharSequence} message object in the status view. */ + public static final int MSG_UI_SET_STATUS_TEXT = 10; + /** Displays a toast signalling that a re-scan is required. Does not expect a message object. */ + public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11; + + private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug"; + + private final Context mContext; + private final Handler mHandler; + private final View mOverlayView; + private final TextView mMessageView; + private final TextView mStatusView; + private final TextView mAudioStatusView; + private final ViewGroup mMessageLayout; + private final CaptionTrackRenderer mCaptionTrackRenderer; + + /** + * Creates and inflates a {@link Session} overlay from the given context. + * + * @param context The {@link Context} of the {@link Session}. + */ + public TunerSessionOverlay(Context context) { + mContext = context; + mHandler = new Handler(this); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false); + mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null); + mMessageLayout = mOverlayView.findViewById(R.id.message_layout); + mMessageLayout.setVisibility(View.INVISIBLE); + mMessageView = mOverlayView.findViewById(R.id.message); + mStatusView = mOverlayView.findViewById(R.id.tuner_status); + mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE); + mAudioStatusView = mOverlayView.findViewById(R.id.audio_status); + mAudioStatusView.setVisibility(View.INVISIBLE); + CaptionLayout captionLayout = mOverlayView.findViewById(R.id.caption); + mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); + } + + /** Clears any pending messages in the message queue. */ + public void release() { + mHandler.removeCallbacksAndMessages(null); + } + + /** Returns a {@link View} representation of the overlay. */ + public View getOverlayView() { + return mOverlayView; + } + + /** + * Posts a message to be handled on the main thread. Only messages that do not expect a message + * object may be posted through this method. + * + * @param message One of the {@code MSG_UI_*} constants. + */ + public void sendUiMessage(int message) { + mHandler.sendEmptyMessage(message); + } + + /** + * Posts a message to be handled on the main thread. + * + * @param message One of the {@code MSG_UI_*} constants. + * @param object The object of the message. The required message object type depends on the + * message being posted. + */ + public void sendUiMessage(int message, Object object) { + mHandler.obtainMessage(message, object).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_UI_SHOW_MESSAGE: + mMessageView.setText((String) msg.obj); + mMessageLayout.setVisibility(View.VISIBLE); + return true; + case MSG_UI_HIDE_MESSAGE: + mMessageLayout.setVisibility(View.INVISIBLE); + return true; + case MSG_UI_SHOW_AUDIO_UNPLAYABLE: + // Showing message of enabling surround sound only when global surround sound + // setting is "never". + final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); + if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { + mAudioStatusView.setText( + Html.fromHtml( + StatusTextUtils.getAudioWarningInHTML( + mContext.getString( + R.string.ut_surround_sound_disabled)))); + } else { + mAudioStatusView.setText( + Html.fromHtml( + StatusTextUtils.getAudioWarningInHTML( + mContext.getString( + R.string.audio_passthrough_not_supported)))); + } + mAudioStatusView.setVisibility(View.VISIBLE); + return true; + case MSG_UI_HIDE_AUDIO_UNPLAYABLE: + mAudioStatusView.setVisibility(View.INVISIBLE); + return true; + case MSG_UI_PROCESS_CAPTION_TRACK: + mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj); + return true; + case MSG_UI_START_CAPTION_TRACK: + mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj); + return true; + case MSG_UI_STOP_CAPTION_TRACK: + mCaptionTrackRenderer.stop(); + return true; + case MSG_UI_RESET_CAPTION_TRACK: + mCaptionTrackRenderer.reset(); + return true; + case MSG_UI_CLEAR_CAPTION_RENDERER: + mCaptionTrackRenderer.clear(); + return true; + case MSG_UI_SET_STATUS_TEXT: + mStatusView.setText((CharSequence) msg.obj); + return true; + case MSG_UI_TOAST_RESCAN_NEEDED: + Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show(); + return true; + default: + return false; + } + } +} diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java index 65750e08..d3f9409b 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java @@ -34,6 +34,8 @@ import android.os.Message; import android.os.SystemClock; import android.support.annotation.AnyThread; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.Html; import android.text.TextUtils; @@ -45,10 +47,12 @@ import android.view.accessibility.CaptioningManager; import com.android.tv.common.CommonPreferences.TrickplaySetting; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.compat.TvInputConstantCompat; import com.android.tv.common.customization.CustomizationManager; import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE; +import com.android.tv.common.experiments.Experiments; +import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.util.SystemPropertiesProxy; -import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.PsipData.TvTracksInterface; @@ -61,12 +65,19 @@ import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; +import com.android.tv.tuner.prefs.TunerPreferences; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.ts.EventDetector.EventListener; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.tuner.tvinput.debug.TunerDebug; import com.android.tv.tuner.util.StatusTextUtils; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.common.collect.ImmutableList; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; import java.io.File; import java.util.ArrayList; import java.util.Iterator; @@ -84,9 +95,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, - EventDetector.EventListener, + EventListener, ChannelDataManager.ProgramInfoListener, Handler.Callback { + private static final String TAG = "TunerSessionWorker"; private static final boolean DEBUG = false; private static final boolean ENABLE_PROFILER = true; @@ -103,14 +115,13 @@ public class TunerSessionWorker public static final int MSG_TIMESHIFT_RESUME = 5; public static final int MSG_TIMESHIFT_SEEK_TO = 6; public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7; - public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8; public static final int MSG_UNBLOCKED_RATING = 9; public static final int MSG_TUNER_PREFERENCES_CHANGED = 10; // Private messages - private static final int MSG_TUNE = 1000; + @VisibleForTesting protected static final int MSG_TUNE = 1000; private static final int MSG_RELEASE = 1001; - private static final int MSG_RETRY_PLAYBACK = 1002; + @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002; private static final int MSG_START_PLAYBACK = 1003; private static final int MSG_UPDATE_PROGRAM = 1008; private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009; @@ -120,7 +131,7 @@ public class TunerSessionWorker private static final int MSG_PARENTAL_CONTROLS = 1015; private static final int MSG_RESCHEDULE_PROGRAMS = 1016; private static final int MSG_BUFFER_START_TIME_CHANGED = 1017; - private static final int MSG_CHECK_SIGNAL = 1018; + @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018; private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019; private static final int MSG_RESET_PLAYBACK = 1020; private static final int MSG_BUFFER_STATE_CHANGED = 1021; @@ -128,6 +139,7 @@ public class TunerSessionWorker private static final int MSG_STOP_TUNE = 1023; private static final int MSG_SET_SURFACE = 1024; private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025; + @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026; private static final int TS_PACKET_SIZE = 188; private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000; @@ -137,6 +149,7 @@ public class TunerSessionWorker private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000; private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000; private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000; + private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000; // The following 3s is defined empirically. This should be larger than 2s considering video // key frame interval in the TS stream. private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000; @@ -162,6 +175,8 @@ public class TunerSessionWorker private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250; private static final int RELEASE_WAIT_INTERVAL_MS = 50; private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14); + private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); + public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of(); // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker // creation/release is required. @@ -202,10 +217,12 @@ public class TunerSessionWorker private boolean mChannelBlocked; private TvContentRating mUnblockedContentRating; private long mLastPositionMs; + private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver; private AudioCapabilities mAudioCapabilities; private long mLastLimitInBytes; private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); private final TunerSession mSession; + private final TunerSessionOverlay mTunerSessionOverlay; private final boolean mHasSoftwareAudioDecoder; private int mPlayerState = ExoPlayer.STATE_IDLE; private long mPreparingStartTimeMs; @@ -214,24 +231,62 @@ public class TunerSessionWorker private boolean mIsActiveSession; private boolean mReleaseRequested; // Guarded by mReleaseLock private final Object mReleaseLock = new Object(); + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + + private int mSignalStrength; + private long mRecordedProgramStartTimeMs; public TunerSessionWorker( - Context context, ChannelDataManager channelDataManager, TunerSession tunerSession) { + Context context, + ChannelDataManager channelDataManager, + TunerSession tunerSession, + TunerSessionOverlay tunerSessionOverlay, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + this( + context, + channelDataManager, + tunerSession, + tunerSessionOverlay, + null, + concurrentDvrPlaybackFlags, + tsDataSourceManagerFactory); + } + + @VisibleForTesting + protected TunerSessionWorker( + Context context, + ChannelDataManager channelDataManager, + TunerSession tunerSession, + TunerSessionOverlay tunerSessionOverlay, + @Nullable Handler handler, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + this.mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; if (DEBUG) Log.d(TAG, "TunerSessionWorker created"); mContext = context; - - // HandlerThread should be set up before it is registered as a listener in the all other - // components. - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); + if (handler != null) { + mHandler = handler; + } else { + // HandlerThread should be set up before it is registered as a listener in the all other + // components. + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + } mSession = tunerSession; + mTunerSessionOverlay = tunerSessionOverlay; mChannelDataManager = channelDataManager; mChannelDataManager.setListener(this); mChannelDataManager.checkDataVersion(mContext); - mSourceManager = TsDataSourceManager.createSourceManager(false); + mSourceManager = tsDataSourceManagerFactory.create(false); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mTvTracks = new ArrayList<>(); + mAudioCapabilitiesReceiver = + new AudioCapabilitiesReceiverV1Wrapper( + context, mHandler, this::handleMessageAudioCapabilitiesChanged); + AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register(); + mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities)); mAudioTrackMap = new SparseArray<>(); mCaptionTrackMap = new SparseArray<>(); CaptioningManager captioningManager = @@ -401,6 +456,7 @@ public class TunerSessionWorker // TODO reimplement for google3 // Here disconnect ffmpeg } + mAudioCapabilitiesReceiver.unregister(); mChannelDataManager.setListener(null); mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_RELEASE); @@ -509,18 +565,18 @@ public class TunerSessionWorker return; } Log.i(TAG, "AC3 audio cannot be played due to device limitation"); - mSession.sendUiMessage(TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE); } // MpegTsPlayer.VideoEventListener @Override public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { - mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event); } @Override public void onClearCaptionEvent() { - mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER); } @Override @@ -541,7 +597,7 @@ public class TunerSessionWorker @Override public void onRescanNeeded() { - mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED); } @Override @@ -596,10 +652,12 @@ public class TunerSessionWorker private static class RecordedProgram { // private final long mChannelId; private final String mDataUri; + private final long mStartTimeMillis; private static final String[] PROJECTION = { TvContract.Programs.COLUMN_CHANNEL_ID, TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, + TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, }; public RecordedProgram(Cursor cursor) { @@ -607,11 +665,13 @@ public class TunerSessionWorker // mChannelId = cursor.getLong(index++); index++; mDataUri = cursor.getString(index++); + mStartTimeMillis = cursor.getLong(index++); } public RecordedProgram(long channelId, String dataUri) { // mChannelId = channelId; mDataUri = dataUri; + mStartTimeMillis = 0; } public static RecordedProgram onQuery(Cursor c) { @@ -625,6 +685,10 @@ public class TunerSessionWorker public String getDataUri() { return mDataUri; } + + public long getStartTime() { + return mStartTimeMillis; + } } private RecordedProgram getRecordedProgram(Uri recordedUri) { @@ -650,6 +714,7 @@ public class TunerSessionWorker private String parseRecording(Uri uri) { RecordedProgram recording = getRecordedProgram(uri); if (recording != null) { + mRecordedProgramStartTimeMs = recording.getStartTime(); return recording.getDataUri(); } return null; @@ -659,514 +724,630 @@ public class TunerSessionWorker public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_TUNE: - { - if (DEBUG) Log.d(TAG, "MSG_TUNE"); - - // When sequential tuning messages arrived, it skips middle tuning messages in - // order - // to change to the last requested channel quickly. - if (mHandler.hasMessages(MSG_TUNE)) { - return true; - } - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); - if (!mIsActiveSession) { - // Wait until release is finished if there is a pending release. - try { - while (!sActiveSessionSemaphore.tryAcquire( - RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { - synchronized (mReleaseLock) { - if (mReleaseRequested) { - return true; - } - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - synchronized (mReleaseLock) { - if (mReleaseRequested) { - sActiveSessionSemaphore.release(); - return true; - } - } - mIsActiveSession = true; - } - Uri channelUri = (Uri) msg.obj; - String recording = null; - long channelId = parseChannel(channelUri); - TunerChannel channel = - (channelId == -1) ? null : mChannelDataManager.getChannel(channelId); - if (channelId == -1) { - recording = parseRecording(channelUri); - } - if (channel == null && recording == null) { - Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); - stopTune(); - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); - return true; - } - clearCallbacksAndMessagesSafely(); - mChannelDataManager.removeAllCallbacksAndMessages(); - if (channel != null) { - if (mTvInputManager.isParentalControlsEnabled() && channel.isLocked()) { - Log.i(TAG, "onTune() is failed. Channel is blocked" + channel); - mSession.notifyContentBlocked(TvContentRating.UNRATED); - return true; - } - mChannelDataManager.requestProgramsData(channel); - } - prepareTune(channel, recording); - // TODO: Need to refactor. notifyContentAllowed() should not be called if - // parental - // control is turned on. - mSession.notifyContentAllowed(); - resetTvTracks(); - resetPlayback(); - mHandler.sendEmptyMessageDelayed( - MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); - return true; - } + return handleMessageTune((Uri) msg.obj); case MSG_STOP_TUNE: - { - if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE"); - mChannel = null; - stopPlayback(true); - stopCaptionTrack(); - resetTvTracks(); - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); - return true; - } + return handleMessageStopTune(); case MSG_RELEASE: - { - if (DEBUG) Log.d(TAG, "MSG_RELEASE"); - mHandler.removeCallbacksAndMessages(null); - stopPlayback(true); - stopCaptionTrack(); - mSourceManager.release(); - mHandler.getLooper().quitSafely(); - if (mIsActiveSession) { - sActiveSessionSemaphore.release(); - } - return true; - } + return handleMessageRelease(); case MSG_RETRY_PLAYBACK: - { - if (System.identityHashCode(mPlayer) == (int) msg.obj) { - Log.i(TAG, "Retrying the playback for channel: " + mChannel); - mHandler.removeMessages(MSG_RETRY_PLAYBACK); - // When there is a request of retrying playback, don't reuse TunerHal. - mSourceManager.setKeepTuneStatus(false); - mRetryCount++; - if (DEBUG) { - Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); - } - mChannelDataManager.removeAllCallbacksAndMessages(); - if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { - resetPlayback(); - } else { - // When it reaches this point, it may be due to an error that occurred - // in - // the tuner device. Calling stopPlayback() resets the tuner device - // to recover from the error. - stopPlayback(false); - stopCaptionTrack(); - - notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - Log.i(TAG, "Notify weak signal since fail to retry playback"); - - // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically - // chosen - // value before recovering the playback. - mHandler.sendEmptyMessageDelayed( - MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS); - } - } - return true; - } + return handleMessageRetryPlayback((int) msg.obj); case MSG_RESET_PLAYBACK: - { - if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK"); - mChannelDataManager.removeAllCallbacksAndMessages(); - resetPlayback(); - return true; - } + return handleMessageResetPlayback(); case MSG_START_PLAYBACK: - { - if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK"); - if (mChannel != null || mRecordingId != null) { - startPlayback((int) msg.obj); - } - return true; - } + return handleMessageStartPlayback((int) msg.obj); case MSG_UPDATE_PROGRAM: - { - if (mChannel != null) { - EitItem program = (EitItem) msg.obj; - updateTvTracks(program, false); - mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); - } - return true; - } + return handleMessageUpdateProgram((EitItem) msg.obj); case MSG_SCHEDULE_OF_PROGRAMS: - { - mHandler.removeMessages(MSG_UPDATE_PROGRAM); - Pair<TunerChannel, List<EitItem>> pair = - (Pair<TunerChannel, List<EitItem>>) msg.obj; - TunerChannel channel = pair.first; - if (mChannel == null) { - return true; - } - if (mChannel != null && mChannel.compareTo(channel) != 0) { - return true; - } - mPrograms = pair.second; - EitItem currentProgram = getCurrentProgram(); - if (currentProgram == null) { - mProgram = null; - } - long currentTimeMs = getCurrentPosition(); - if (mPrograms != null) { - for (EitItem item : mPrograms) { - if (currentProgram != null && currentProgram.compareTo(item) == 0) { - if (DEBUG) { - Log.d(TAG, "Update current TvTracks " + item); - } - if (mProgram != null && mProgram.compareTo(item) == 0) { - continue; - } - mProgram = item; - updateTvTracks(item, false); - } else if (item.getStartTimeUtcMillis() > currentTimeMs) { - if (DEBUG) { - Log.d( - TAG, - "Update next TvTracks " - + item - + " " - + (item.getStartTimeUtcMillis() - - currentTimeMs)); - } - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item), - item.getStartTimeUtcMillis() - currentTimeMs); - } - } - } - mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); - return true; - } + // TODO: fix the unchecked cast waring. + Pair<TunerChannel, List<EitItem>> pair = + (Pair<TunerChannel, List<EitItem>>) msg.obj; + return handleMessageScheduleOfPrograms(pair); case MSG_UPDATE_CHANNEL_INFO: - { - TunerChannel channel = (TunerChannel) msg.obj; - if (mChannel != null && mChannel.compareTo(channel) == 0) { - updateChannelInfo(channel); - } - return true; - } + return handleMessageUpdateChannelInfo((TunerChannel) msg.obj); case MSG_PROGRAM_DATA_RESULT: - { - TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first; - - // If there already exists, skip it since real-time data is a top priority, - if (mChannel != null - && mChannel.compareTo(channel) == 0 - && mPrograms == null - && mProgram == null) { - sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj); - } - return true; - } + return handleMessageProgramDataResult(msg); case MSG_TRICKPLAY_BY_SEEK: - { - if (mPlayer == null) { - return true; - } - doTrickplayBySeek(msg.arg1); - return true; - } + return handleMessageTrickplayBySeek(msg.arg1); case MSG_SMOOTH_TRICKPLAY_MONITOR: - { - if (mPlayer == null) { - return true; - } - long systemCurrentTime = System.currentTimeMillis(); - long position = getCurrentPosition(); - if (mRecordingId == null) { - // Checks if the position exceeds the upper bound when forwarding, - // or exceed the lower bound when rewinding. - // If the direction is not checked, there can be some issues. - // (See b/29939781 for more details.) - if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L) - || (position < mBufferStartTimeMs - && mPlaybackParams.getSpeed() < 0L)) { - doTimeShiftResume(); - return true; - } - } else { - if (position > mRecordingDuration || position < 0) { - doTimeShiftPause(); - return true; - } - } - mHandler.sendEmptyMessageDelayed( - MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); - return true; - } + return handleMessageSmoothTrickplayMonitor(); case MSG_RESCHEDULE_PROGRAMS: - { - if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { - mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); - } else { - doReschedulePrograms(); - } - return true; - } + return handleMessageReschedulePrograms(); case MSG_PARENTAL_CONTROLS: - { - doParentalControls(); - mHandler.removeMessages(MSG_PARENTAL_CONTROLS); - mHandler.sendEmptyMessageDelayed( - MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); - return true; - } + return handleMessageParentalControl(); case MSG_UNBLOCKED_RATING: - { - mUnblockedContentRating = (TvContentRating) msg.obj; - doParentalControls(); - mHandler.removeMessages(MSG_PARENTAL_CONTROLS); - mHandler.sendEmptyMessageDelayed( - MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); - return true; - } + return handleMessageUnblockedRating((TvContentRating) msg.obj); case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: - { - int serviceNumber = (int) msg.obj; - doDiscoverCaptionServiceNumber(serviceNumber); - return true; - } + return handleMessageDiscoverCaptionServiceNumber((int) msg.obj); case MSG_SELECT_TRACK: - { - if (mPlayer == null) { - Log.w(TAG, "mPlayer is null when doselectTrack is called"); - return false; - } - if (mChannel != null || mRecordingId != null) { - doSelectTrack(msg.arg1, (String) msg.obj); - } - return true; - } + return handleMessageSelectTrack(msg.arg1, (String) msg.obj); case MSG_UPDATE_CAPTION_TRACK: - { - if (mCaptionEnabled) { - startCaptionTrack(); - } else { - stopCaptionTrack(); - } - return true; - } + return handleMessageUpdateCaptionTrack(); case MSG_TIMESHIFT_PAUSE: - { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftPause(); - return true; - } + return handleMessageTimeshiftPause(); case MSG_TIMESHIFT_RESUME: - { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftResume(); - return true; - } + return handleMessageTimeshiftResume(); case MSG_TIMESHIFT_SEEK_TO: - { - long position = (long) msg.obj; - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftSeekTo(position); - return true; - } + return handleMessageTimeshiftSeekTo((long) msg.obj); case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: - { - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj); - return true; - } - case MSG_AUDIO_CAPABILITIES_CHANGED: - { - AudioCapabilities capabilities = (AudioCapabilities) msg.obj; - if (DEBUG) { - Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities); - } - if (capabilities == null) { - return true; - } - if (!capabilities.equals(mAudioCapabilities)) { - // HDMI supported encodings are changed. restart player. - mAudioCapabilities = capabilities; - resetPlayback(); - } - return true; - } + return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj); case MSG_SET_STREAM_VOLUME: - { - if (mPlayer != null && mPlayer.isPlaying()) { - mPlayer.setVolume(mVolume); - } - return true; - } + return handleMessageSetStreamVolume(); case MSG_TUNER_PREFERENCES_CHANGED: - { - mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED); - @TrickplaySetting - int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext); - if (trickplaySetting != mTrickplaySetting) { - boolean wasTrcikplayEnabled = - mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; - boolean isTrickplayEnabled = - trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; - mTrickplaySetting = trickplaySetting; - if (isTrickplayEnabled != wasTrcikplayEnabled) { - sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer)); - } - } - return true; - } + return handleMessageTunerPreferencesChanged(); case MSG_BUFFER_START_TIME_CHANGED: - { - if (mPlayer == null) { - return true; - } - mBufferStartTimeMs = (long) msg.obj; - if (!hasEnoughBackwardBuffer() - && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) { - mPlayer.setPlayWhenReady(true); - mPlayer.setAudioTrackAndClosedCaption(true); - mPlaybackParams.setSpeed(1.0f); - } - return true; - } + return handleMessageBufferStartTimeChanged((long) msg.obj); case MSG_BUFFER_STATE_CHANGED: - { - boolean available = (boolean) msg.obj; - mSession.notifyTimeShiftStatusChanged( - available - ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE - : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); - return true; - } + return handleMessageBufferStateChanged((boolean) msg.obj); case MSG_CHECK_SIGNAL: - if (mChannel == null || mPlayer == null) { - return true; + return handleMessageCheckSignal(); + case MSG_SET_SURFACE: + return handleMessageSetSurface(); + case MSG_NOTIFY_AUDIO_TRACK_UPDATED: + return handleMessageAudioTrackUpdated(); + case MSG_CHECK_SIGNAL_STRENGTH: + return handleMessageCheckSignalStrength(); + default: + return unhandledMessage(msg); + } + } + + private boolean handleMessageTune(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "MSG_TUNE"); + } + + // When sequential tuning messages arrived, it skips middle tuning messages in + // order + // to change to the last requested channel quickly. + if (mHandler.hasMessages(MSG_TUNE)) { + return true; + } + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); + if (!mIsActiveSession) { + // Wait until release is finished if there is a pending release. + try { + while (!sActiveSessionSemaphore.tryAcquire( + RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { + synchronized (mReleaseLock) { + if (mReleaseRequested) { + return true; + } + } } - TsDataSource source = mPlayer.getDataSource(); - long limitInBytes = source != null ? source.getBufferedPosition() : 0L; - if (TunerDebug.ENABLED) { - TunerDebug.calculateDiff(); - mSession.sendUiMessage( - TunerSession.MSG_UI_SET_STATUS_TEXT, - Html.fromHtml( - StatusTextUtils.getStatusWarningInHTML( - (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, - TunerDebug.getVideoFrameDrop(), - TunerDebug.getBytesInQueue(), - TunerDebug.getAudioPositionUs(), - TunerDebug.getAudioPositionUsRate(), - TunerDebug.getAudioPtsUs(), - TunerDebug.getAudioPtsUsRate(), - TunerDebug.getVideoPtsUs(), - TunerDebug.getVideoPtsUsRate()))); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + synchronized (mReleaseLock) { + if (mReleaseRequested) { + sActiveSessionSemaphore.release(); + return true; } - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); - long currentTime = SystemClock.elapsedRealtime(); - long bufferingTimeMs = - mBufferingStartTimeMs != INVALID_TIME - ? currentTime - mBufferingStartTimeMs - : mBufferingStartTimeMs; - long preparingTimeMs = - mPreparingStartTimeMs != INVALID_TIME - ? currentTime - mPreparingStartTimeMs - : mPreparingStartTimeMs; - boolean isBufferingTooLong = - bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - boolean isPreparingTooLong = - preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - boolean isWeakSignal = - source != null - && mChannel.getType() != Channel.TunerType.TYPE_FILE - && (isBufferingTooLong || isPreparingTooLong); - if (isWeakSignal && !mReportedWeakSignal) { - if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { - mHandler.sendMessageDelayed( - mHandler.obtainMessage( - MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), - PLAYBACK_RETRY_DELAY_MS); + } + mIsActiveSession = true; + } + String recording = null; + long channelId = parseChannel(channelUri); + TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId); + if (channelId == -1) { + recording = parseRecording(channelUri); + } + if (channel == null && recording == null) { + Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); + stopTune(); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + return true; + } + clearCallbacksAndMessagesSafely(); + mChannelDataManager.removeAllCallbacksAndMessages(); + if (channel != null) { + mChannelDataManager.requestProgramsData(channel); + } + prepareTune(channel, recording); + // TODO: Need to refactor. notifyContentAllowed() should not be called if + // parental + // control is turned on. + mSession.notifyContentAllowed(); + resetTvTracks(); + resetPlayback(); + mHandler.sendEmptyMessageDelayed( + MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); + return true; + } + + private boolean handleMessageStopTune() { + if (DEBUG) { + Log.d(TAG, "MSG_STOP_TUNE"); + } + mChannel = null; + stopPlayback(true); + stopCaptionTrack(); + resetTvTracks(); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + return true; + } + + private boolean handleMessageRelease() { + if (DEBUG) { + Log.d(TAG, "MSG_RELEASE"); + } + mHandler.removeCallbacksAndMessages(null); + stopPlayback(true); + stopCaptionTrack(); + mSourceManager.release(); + mHandler.getLooper().quitSafely(); + if (mIsActiveSession) { + sActiveSessionSemaphore.release(); + } + return true; + } + + private boolean handleMessageRetryPlayback(int code) { + if (System.identityHashCode(mPlayer) == code) { + Log.i(TAG, "Retrying the playback for channel: " + mChannel); + mHandler.removeMessages(MSG_RETRY_PLAYBACK); + // When there is a request of retrying playback, don't reuse TunerHal. + mSourceManager.setKeepTuneStatus(false); + mRetryCount++; + if (DEBUG) { + Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); + } + mChannelDataManager.removeAllCallbacksAndMessages(); + if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { + resetPlayback(); + } else { + // When it reaches this point, it may be due to an error that occurred + // in + // the tuner device. Calling stopPlayback() resets the tuner device + // to recover from the error. + stopPlayback(false); + stopCaptionTrack(); + + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + Log.i(TAG, "Notify weak signal since fail to retry playback"); + + // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically + // chosen + // value before recovering the playback. + mHandler.sendEmptyMessageDelayed( + MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS); + } + } + return true; + } + + private boolean handleMessageResetPlayback() { + if (DEBUG) { + Log.d(TAG, "MSG_RESET_PLAYBACK"); + } + mChannelDataManager.removeAllCallbacksAndMessages(); + resetPlayback(); + return true; + } + + private boolean handleMessageStartPlayback(int playerHashCode) { + if (DEBUG) { + Log.d(TAG, "MSG_START_PLAYBACK"); + } + if (mChannel != null || mRecordingId != null) { + startPlayback(playerHashCode); + } + return true; + } + + private boolean handleMessageUpdateProgram(EitItem program) { + if (mChannel != null) { + updateTvTracks(program, false); + mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); + } + return true; + } + + private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) { + mHandler.removeMessages(MSG_UPDATE_PROGRAM); + TunerChannel channel = pair.first; + if (mChannel == null) { + return true; + } + if (mChannel != null && mChannel.compareTo(channel) != 0) { + return true; + } + mPrograms = pair.second; + EitItem currentProgram = getCurrentProgram(); + if (currentProgram == null) { + mProgram = null; + } + long currentTimeMs = getCurrentPosition(); + if (mPrograms != null) { + for (EitItem item : mPrograms) { + if (currentProgram != null && currentProgram.compareTo(item) == 0) { + if (DEBUG) { + Log.d(TAG, "Update current TvTracks " + item); } - if (mPlayer != null) { - mPlayer.setAudioTrackAndClosedCaption(false); + if (mProgram != null && mProgram.compareTo(item) == 0) { + continue; } - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - Log.i( - TAG, - "Notify weak signal due to signal check, " - + String.format( - "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " - + "videoFrameDrop:%d", - (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, - bufferingTimeMs, - preparingTimeMs, - TunerDebug.getVideoFrameDrop())); - } else if (!isWeakSignal && mReportedWeakSignal) { - boolean isPlaybackStable = - mReadyStartTimeMs != INVALID_TIME - && currentTime - mReadyStartTimeMs - > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - if (!isPlaybackStable) { - // Wait until playback becomes stable. - } else if (mReportedDrawnToSurface) { - mHandler.removeMessages(MSG_RETRY_PLAYBACK); - notifyVideoAvailable(); - mPlayer.setAudioTrackAndClosedCaption(true); + mProgram = item; + updateTvTracks(item, false); + } else if (item.getStartTimeUtcMillis() > currentTimeMs) { + if (DEBUG) { + Log.d( + TAG, + "Update next TvTracks " + + item + + " " + + (item.getStartTimeUtcMillis() - currentTimeMs)); } + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item), + item.getStartTimeUtcMillis() - currentTimeMs); } - mLastLimitInBytes = limitInBytes; - mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); + } + } + mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); + return true; + } + + private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) { + if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) { + updateChannelInfo(tunerChannel); + } + return true; + } + + private boolean handleMessageProgramDataResult(Message msg) { + TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first; + + // If there already exists, skip it since real-time data is a top priority, + if (mChannel != null + && mChannel.compareTo(channel) == 0 + && mPrograms == null + && mProgram == null) { + sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj); + } + return true; + } + + private boolean handleMessageTrickplayBySeek(int seekPositionMs) { + if (mPlayer == null) { + return true; + } + if (mRecordingId != null) { + long systemBufferTime = + System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs; + if (seekPositionMs > systemBufferTime) { + doTimeShiftResume(); return true; - case MSG_SET_SURFACE: - { - if (mPlayer != null) { - mPlayer.setSurface(mSurface); - } else { - // TODO: Since surface is dynamically set, we can remove the dependency of - // playback start on mSurface nullity. - resetPlayback(); - } - return true; + } + } + doTrickplayBySeek(seekPositionMs); + return true; + } + + private boolean handleMessageSmoothTrickplayMonitor() { + if (mPlayer == null) { + return true; + } + long systemCurrentTime = System.currentTimeMillis(); + long position = getCurrentPosition(); + if (mRecordingId == null) { + // Checks if the position exceeds the upper bound when forwarding, + // or exceed the lower bound when rewinding. + // If the direction is not checked, there can be some issues. + // (See b/29939781 for more details.) + if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L) + || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) { + doTimeShiftResume(); + return true; + } + } else { + if (position > mRecordingDuration || position < 0) { + doTimeShiftPause(); + return true; + } + long systemBufferTime = + systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs; + if (position > systemBufferTime) { + doTimeShiftResume(); + return true; + } + } + mHandler.sendEmptyMessageDelayed( + MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); + return true; + } + + private boolean handleMessageReschedulePrograms() { + if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { + mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); + } else { + doReschedulePrograms(); + } + return true; + } + + private boolean handleMessageParentalControl() { + doParentalControls(); + mHandler.removeMessages(MSG_PARENTAL_CONTROLS); + mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); + return true; + } + + private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) { + mUnblockedContentRating = unblockedContentRating; + return handleMessageParentalControl(); + } + + private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) { + doDiscoverCaptionServiceNumber(serviceNumber); + return true; + } + + private boolean handleMessageSelectTrack(int type, String trackId) { + if (mPlayer == null) { + Log.w(TAG, "mPlayer is null when doselectTrack is called"); + return false; + } + if (mChannel != null || mRecordingId != null) { + doSelectTrack(type, trackId); + } + return true; + } + + private boolean handleMessageUpdateCaptionTrack() { + if (mCaptionEnabled) { + startCaptionTrack(); + } else { + stopCaptionTrack(); + } + return true; + } + + private boolean handleMessageTimeshiftPause() { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftPause(); + return true; + } + + private boolean handleMessageTimeshiftResume() { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_RESUME"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftResume(); + return true; + } + + private boolean handleMessageTimeshiftSeekTo(long timeMs) { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftSeekTo(timeMs); + return true; + } + + private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) { + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftSetPlaybackParams(playbackParams); + return true; + } + + private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + if (DEBUG) { + Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities); + } + if (audioCapabilities == null) { + return true; + } + if (!audioCapabilities.equals(mAudioCapabilities)) { + // HDMI supported encodings are changed. restart player. + mAudioCapabilities = audioCapabilities; + resetPlayback(); + } + return true; + } + + private boolean handleMessageSetStreamVolume() { + if (mPlayer != null && mPlayer.isPlaying()) { + mPlayer.setVolume(mVolume); + } + return true; + } + + private boolean handleMessageTunerPreferencesChanged() { + mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED); + @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext); + if (trickplaySetting != mTrickplaySetting) { + boolean wasTrcikplayEnabled = + mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; + boolean isTrickplayEnabled = + trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; + mTrickplaySetting = trickplaySetting; + if (isTrickplayEnabled != wasTrcikplayEnabled) { + sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer)); + } + } + return true; + } + + private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) { + if (mPlayer == null) { + return true; + } + mBufferStartTimeMs = bufferStartTimeMs; + if (!hasEnoughBackwardBuffer() + && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) { + mPlayer.setPlayWhenReady(true); + mPlayer.setAudioTrackAndClosedCaption(true); + mPlaybackParams.setSpeed(1.0f); + } + return true; + } + + private boolean handleMessageBufferStateChanged(boolean available) { + mSession.notifyTimeShiftStatusChanged( + available + ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE + : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); + return true; + } + + private boolean handleMessageCheckSignal() { + if (mChannel == null || mPlayer == null) { + return true; + } + TsDataSource source = mPlayer.getDataSource(); + long limitInBytes = source != null ? source.getBufferedPosition() : 0L; + if (TunerDebug.ENABLED) { + TunerDebug.calculateDiff(); + mTunerSessionOverlay.sendUiMessage( + TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT, + Html.fromHtml( + StatusTextUtils.getStatusWarningInHTML( + (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, + TunerDebug.getVideoFrameDrop(), + TunerDebug.getBytesInQueue(), + TunerDebug.getAudioPositionUs(), + TunerDebug.getAudioPositionUsRate(), + TunerDebug.getAudioPtsUs(), + TunerDebug.getAudioPtsUsRate(), + TunerDebug.getVideoPtsUs(), + TunerDebug.getVideoPtsUsRate()))); + } + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE); + long currentTime = SystemClock.elapsedRealtime(); + long bufferingTimeMs = + mBufferingStartTimeMs != INVALID_TIME + ? currentTime - mBufferingStartTimeMs + : mBufferingStartTimeMs; + long preparingTimeMs = + mPreparingStartTimeMs != INVALID_TIME + ? currentTime - mPreparingStartTimeMs + : mPreparingStartTimeMs; + boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + boolean isWeakSignal = + source != null + && mChannel.getType() != Channel.TunerType.TYPE_FILE + && (isBufferingTooLong || isPreparingTooLong); + if (isWeakSignal && !mReportedWeakSignal) { + if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), + PLAYBACK_RETRY_DELAY_MS); + } + if (mPlayer != null) { + mPlayer.setAudioTrackAndClosedCaption(false); + } + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mSession.notifySignalStrength(0); + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); + } + Log.i( + TAG, + "Notify weak signal due to signal check, " + + String.format( + "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " + + "videoFrameDrop:%d", + (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, + bufferingTimeMs, + preparingTimeMs, + TunerDebug.getVideoFrameDrop())); + } else if (!isWeakSignal && mReportedWeakSignal) { + boolean isPlaybackStable = + mReadyStartTimeMs != INVALID_TIME + && currentTime - mReadyStartTimeMs + > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + if (!isPlaybackStable) { + // Wait until playback becomes stable. + } else if (mReportedDrawnToSurface) { + mHandler.removeMessages(MSG_RETRY_PLAYBACK); + notifyVideoAvailable(); + mPlayer.setAudioTrackAndClosedCaption(true); + if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) { + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); } - case MSG_NOTIFY_AUDIO_TRACK_UPDATED: - { - notifyAudioTracksUpdated(); - return true; + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + sendMessage(MSG_CHECK_SIGNAL_STRENGTH); } - default: - { - Log.w(TAG, "Unhandled message code: " + msg.what); - return false; + } + } + mLastLimitInBytes = limitInBytes; + mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); + return true; + } + + private boolean handleMessageSetSurface() { + if (mPlayer != null) { + mPlayer.setSurface(mSurface); + } else { + // TODO: Since surface is dynamically set, we can remove the dependency of + // playback start on mSurface nullity. + resetPlayback(); + } + return true; + } + + private boolean handleMessageAudioTrackUpdated() { + notifyAudioTracksUpdated(); + return true; + } + + private boolean handleMessageCheckSignalStrength() { + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + int signal; + if (mPlayer != null) { + TsDataSource source = mPlayer.getDataSource(); + if (source != null) { + signal = source.getSignalStrength(); + return handleSignal(signal); } + } + } + return false; + } + + @VisibleForTesting + protected boolean handleSignal(int signal) { + if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED + || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) { + notifySignal(signal); + return true; + } + if (signal != mSignalStrength && signal >= 0) { + notifySignal(signal); } + mHandler.sendEmptyMessageDelayed( + MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS); + return true; + } + + @VisibleForTesting + protected void notifySignal(int signal) { + mSession.notifySignalStrength(signal); + mSignalStrength = signal; + } + + private boolean unhandledMessage(Message msg) { + Log.w(TAG, "Unhandled message code: " + msg.what); + return false; } // Private methods @@ -1212,10 +1393,12 @@ public class TunerSessionWorker } } - private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { + @VisibleForTesting + protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) { if (capabilities == null) { Log.w(TAG, "No Audio Capabilities"); } + mSourceManager.setKeepTuneStatus(true); long now = System.currentTimeMillis(); if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { @@ -1249,19 +1432,27 @@ public class TunerSessionWorker } MpegTsPlayer player = new MpegTsPlayer( - new MpegTsRendererBuilder(mContext, bufferManager, this), + new MpegTsRendererBuilder( + mContext, bufferManager, this, mConcurrentDvrPlaybackFlags), mHandler, mSourceManager, capabilities, this); Log.i(TAG, "Passthrough AC3 renderer"); if (DEBUG) Log.d(TAG, "ExoPlayer created"); + player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); + player.setVideoEventListener(this); + player.setCaptionServiceNumber( + mCaptionTrack != null + ? mCaptionTrack.serviceNumber + : Cea708Data.EMPTY_SERVICE_NUMBER); return player; } private void startCaptionTrack() { if (mCaptionEnabled && mCaptionTrack != null) { - mSession.sendUiMessage(TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); + mTunerSessionOverlay.sendUiMessage( + TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); if (mPlayer != null) { mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber); } @@ -1272,14 +1463,14 @@ public class TunerSessionWorker if (mPlayer != null) { mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); } - mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK); } private void resetTvTracks() { mTvTracks.clear(); mAudioTrackMap.clear(); mCaptionTrackMap.clear(); - mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK); mSession.notifyTracksChanged(mTvTracks); } @@ -1478,7 +1669,7 @@ public class TunerSessionWorker mBufferingStartTimeMs = INVALID_TIME; mReadyStartTimeMs = INVALID_TIME; mLastLimitInBytes = 0L; - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE); mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); } } @@ -1518,24 +1709,14 @@ public class TunerSessionWorker // Doesn't show buffering during weak signal. notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); } - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE); mPlayerStarted = true; } } - private void preparePlayback() { - SoftPreconditions.checkState(mPlayer == null); - if (mChannel == null && mRecordingId == null) { - return; - } - mSourceManager.setKeepTuneStatus(true); + @VisibleForTesting + protected void preparePlayback() { MpegTsPlayer player = createPlayer(mAudioCapabilities); - player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); - player.setVideoEventListener(this); - player.setCaptionServiceNumber( - mCaptionTrack != null - ? mCaptionTrack.serviceNumber - : Cea708Data.EMPTY_SERVICE_NUMBER); if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) { mSourceManager.setKeepTuneStatus(false); player.release(); @@ -1554,6 +1735,12 @@ public class TunerSessionWorker mPlayerStarted = false; mHandler.removeMessages(MSG_CHECK_SIGNAL); mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); + if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) { + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); + } + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH); + } } } @@ -1568,9 +1755,10 @@ public class TunerSessionWorker timestamp = SystemClock.elapsedRealtime(); Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms"); } - if (mChannelBlocked || mSurface == null) { + if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) { return; } + SoftPreconditions.checkState(mPlayer == null); preparePlayback(); } @@ -1591,6 +1779,10 @@ public class TunerSessionWorker } mLastPositionMs = 0; mCaptionTrack = null; + mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN; + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mSession.notifySignalStrength(mSignalStrength); + } mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); } @@ -1793,10 +1985,14 @@ public class TunerSessionWorker if (currentProgram == null) { return null; } - TvContentRating[] ratings = + ImmutableList<TvContentRating> ratings = mTvContentRatingCache.getRatings(currentProgram.getContentRating()); - if (ratings == null || ratings.length == 0) { - ratings = new TvContentRating[] {TvContentRating.UNRATED}; + if ((ratings == null || ratings.isEmpty())) { + if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) { + ratings = ImmutableList.of(TvContentRating.UNRATED); + } else { + ratings = NO_CONTENT_RATINGS; + } } for (TvContentRating rating : ratings) { if (!Objects.equals(mUnblockedContentRating, rating) diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java new file mode 100644 index 00000000..82afff15 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java @@ -0,0 +1,2073 @@ +/* + * Copyright (C) 2015 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.tuner.tvinput; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.media.MediaFormat; +import android.media.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.SystemClock; +import android.support.annotation.AnyThread; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.text.Html; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.view.Surface; +import android.view.accessibility.CaptioningManager; +import com.android.tv.common.CommonPreferences.TrickplaySetting; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.compat.TvInputConstantCompat; +import com.android.tv.common.customization.CustomizationManager; +import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE; +import com.android.tv.common.experiments.Experiments; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.util.SystemPropertiesProxy; +import com.android.tv.tuner.data.Cea708Data; +import com.android.tv.tuner.data.PsipData.EitItem; +import com.android.tv.tuner.data.PsipData.TvTracksInterface; +import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.data.nano.Channel; +import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; +import com.android.tv.tuner.exoplayer.MpegTsPlayer; +import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; +import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager; +import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; +import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; +import com.android.tv.tuner.prefs.TunerPreferences; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.ts.EventDetector.EventListener; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.tuner.tvinput.debug.TunerDebug; +import com.android.tv.tuner.util.StatusTextUtils; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.common.collect.ImmutableList; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** Handles playback related operations on a worker thread. */ +@WorkerThread +public class TunerSessionWorkerExoV2 + implements PlaybackBufferListener, + MpegTsPlayer.VideoEventListener, + MpegTsPlayer.Listener, + EventListener, + ChannelDataManager.ProgramInfoListener, + Handler.Callback { + + private static final String TAG = "TunerSessionWorkerExoV2"; + private static final boolean DEBUG = false; + private static final boolean ENABLE_PROFILER = true; + private static final String PLAY_FROM_CHANNEL = "channel"; + private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; + private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB + private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB + + // Public messages + public static final int MSG_SELECT_TRACK = 1; + public static final int MSG_UPDATE_CAPTION_TRACK = 2; + public static final int MSG_SET_STREAM_VOLUME = 3; + public static final int MSG_TIMESHIFT_PAUSE = 4; + public static final int MSG_TIMESHIFT_RESUME = 5; + public static final int MSG_TIMESHIFT_SEEK_TO = 6; + public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7; + public static final int MSG_UNBLOCKED_RATING = 9; + public static final int MSG_TUNER_PREFERENCES_CHANGED = 10; + + // Private messages + @VisibleForTesting protected static final int MSG_TUNE = 1000; + private static final int MSG_RELEASE = 1001; + @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002; + private static final int MSG_START_PLAYBACK = 1003; + private static final int MSG_UPDATE_PROGRAM = 1008; + private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009; + private static final int MSG_UPDATE_CHANNEL_INFO = 1010; + private static final int MSG_TRICKPLAY_BY_SEEK = 1011; + private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012; + private static final int MSG_PARENTAL_CONTROLS = 1015; + private static final int MSG_RESCHEDULE_PROGRAMS = 1016; + private static final int MSG_BUFFER_START_TIME_CHANGED = 1017; + @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018; + private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019; + private static final int MSG_RESET_PLAYBACK = 1020; + private static final int MSG_BUFFER_STATE_CHANGED = 1021; + private static final int MSG_PROGRAM_DATA_RESULT = 1022; + private static final int MSG_STOP_TUNE = 1023; + private static final int MSG_SET_SURFACE = 1024; + private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025; + @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026; + + private static final int TS_PACKET_SIZE = 188; + private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000; + private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500; + private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500; + private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000; + private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000; + private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000; + private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000; + private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000; + // The following 3s is defined empirically. This should be larger than 2s considering video + // key frame interval in the TS stream. + private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000; + private static final int PLAYBACK_RETRY_DELAY_MS = 5000; + private static final int MAX_IMMEDIATE_RETRY_COUNT = 5; + private static final long INVALID_TIME = -1; + + // Some examples of the track ids of the audio tracks, "a0", "a1", "a2". + // The number after prefix is being used for indicating a index of the given audio track. + private static final String AUDIO_TRACK_PREFIX = "a"; + + // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3". + // The number after prefix is being used for indicating a index of a caption service number + // of the given caption track. + private static final String SUBTITLE_TRACK_PREFIX = "s"; + private static final int TRACK_PREFIX_SIZE = 1; + private static final String VIDEO_TRACK_ID = "v"; + private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000; + + // Actual interval would be divided by the speed. + private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500; + private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20; + private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250; + private static final int RELEASE_WAIT_INTERVAL_MS = 50; + private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14); + private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); + public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of(); + + /** + * Guarantees that at most one active worker exists at any give time. Synchronization between + * multiple TunerSessionWorkerExoV2 is necessary when concurrent release and creation takes + * place. + */ + private static Semaphore sActiveSessionSemaphore = new Semaphore(1); + + private final Context mContext; + private final ChannelDataManager mChannelDataManager; + private final TsDataSourceManager mSourceManager; + private final int mMaxTrickplayBufferSizeMb; + private final File mTrickplayBufferDir; + private final @TRICKPLAY_MODE int mTrickplayModeCustomization; + private volatile Surface mSurface; + private volatile float mVolume = 1.0f; + private volatile boolean mCaptionEnabled; + private volatile MpegTsPlayer mPlayer; + private volatile TunerChannel mChannel; + private volatile Long mRecordingDuration; + private volatile long mRecordStartTimeMs; + private volatile long mBufferStartTimeMs; + private volatile boolean mTrickplayDisabledByStorageIssue; + private @TrickplaySetting int mTrickplaySetting; + private long mTrickplayExpiredMs; + private String mRecordingId; + private final Handler mHandler; + private int mRetryCount; + private final ArrayList<TvTrackInfo> mTvTracks; + private final SparseArray<AtscAudioTrack> mAudioTrackMap; + private final SparseArray<AtscCaptionTrack> mCaptionTrackMap; + private AtscCaptionTrack mCaptionTrack; + private PlaybackParams mPlaybackParams = new PlaybackParams(); + private boolean mPlayerStarted = false; + private boolean mReportedDrawnToSurface = false; + private boolean mReportedWeakSignal = false; + private EitItem mProgram; + private List<EitItem> mPrograms; + private final TvInputManager mTvInputManager; + private boolean mChannelBlocked; + private TvContentRating mUnblockedContentRating; + private long mLastPositionMs; + private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver; + private AudioCapabilities mAudioCapabilities; + private long mLastLimitInBytes; + private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); + private final TunerSessionExoV2 mSession; + private final TunerSessionOverlay mTunerSessionOverlay; + private final boolean mHasSoftwareAudioDecoder; + private int mPlayerState = ExoPlayer.STATE_IDLE; + private long mPreparingStartTimeMs; + private long mBufferingStartTimeMs; + private long mReadyStartTimeMs; + private boolean mIsActiveSession; + private boolean mReleaseRequested; // Guarded by mReleaseLock + private final Object mReleaseLock = new Object(); + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + + private int mSignalStrength; + private long mRecordedProgramStartTimeMs; + + public TunerSessionWorkerExoV2( + Context context, + ChannelDataManager channelDataManager, + TunerSessionExoV2 tunerSession, + TunerSessionOverlay tunerSessionOverlay, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + this( + context, + channelDataManager, + tunerSession, + tunerSessionOverlay, + null, + concurrentDvrPlaybackFlags, + tsDataSourceManagerFactory); + } + + @VisibleForTesting + protected TunerSessionWorkerExoV2( + Context context, + ChannelDataManager channelDataManager, + TunerSessionExoV2 tunerSession, + TunerSessionOverlay tunerSessionOverlay, + @Nullable Handler handler, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; + if (DEBUG) { + Log.d(TAG, "TunerSessionWorkerExoV2 created"); + } + mContext = context; + if (handler != null) { + mHandler = handler; + } else { + // HandlerThread should be set up before it is registered as a listener in the all other + // components. + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + } + mSession = tunerSession; + mTunerSessionOverlay = tunerSessionOverlay; + mChannelDataManager = channelDataManager; + mChannelDataManager.setListener(this); + mChannelDataManager.checkDataVersion(mContext); + mSourceManager = tsDataSourceManagerFactory.create(false); + mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); + mTvTracks = new ArrayList<>(); + mAudioCapabilitiesReceiver = + new AudioCapabilitiesReceiverV1Wrapper( + context, mHandler, this::handleMessageAudioCapabilitiesChanged); + AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register(); + mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities)); + mAudioTrackMap = new SparseArray<>(); + mCaptionTrackMap = new SparseArray<>(); + CaptioningManager captioningManager = + (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); + mCaptionEnabled = captioningManager.isEnabled(); + mPlaybackParams.setSpeed(1.0f); + mMaxTrickplayBufferSizeMb = + SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); + mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context); + if (mTrickplayModeCustomization + == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { + boolean useExternalStorage = + Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + && Environment.isExternalStorageRemovable(); + mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null; + } else if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED) { + mTrickplayBufferDir = context.getCacheDir(); + } else { + mTrickplayBufferDir = null; + } + mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null; + mTrickplaySetting = TunerPreferences.getTrickplaySetting(context); + if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET + && mTrickplayModeCustomization + == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { + // Consider the case of Customization package updates the value of trickplay mode + // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install. + mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET; + TunerPreferences.setTrickplaySetting(context, mTrickplaySetting); + TunerPreferences.setTrickplayExpiredMs(context, 0); + } + mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context); + mPreparingStartTimeMs = INVALID_TIME; + mBufferingStartTimeMs = INVALID_TIME; + mReadyStartTimeMs = INVALID_TIME; + // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time. + // connect() will return false, if there is a connected TunerSessionWorker already. + mHasSoftwareAudioDecoder = false; // TODO reimplement ffmpeg for google3 + // TODO connect the ffmpeg client and report if available. + } + + // Public methods + @MainThread + public void tune(Uri channelUri) { + mHandler.removeCallbacksAndMessages(null); + mSourceManager.setHasPendingTune(); + sendMessage(MSG_TUNE, channelUri); + } + + @MainThread + public void stopTune() { + mHandler.removeCallbacksAndMessages(null); + sendMessage(MSG_STOP_TUNE); + } + + /** Sets {@link Surface}. */ + @MainThread + public void setSurface(Surface surface) { + if (surface != null && !surface.isValid()) { + Log.w(TAG, "Ignoring invalid surface."); + return; + } + // mSurface is kept even when tune is called right after. But, messages can be deleted by + // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message. + mSurface = surface; + mHandler.sendEmptyMessage(MSG_SET_SURFACE); + } + + /** Sets volume. */ + @MainThread + public void setStreamVolume(float volume) { + // mVolume is kept even when tune is called right after. But, messages can be deleted by + // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be + // called in MSG_SET_STREAM_VOLUME. + mVolume = volume; + mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME); + } + + /** Sets if caption is enabled or disabled. */ + @MainThread + public void setCaptionEnabled(boolean captionEnabled) { + // mCaptionEnabled is kept even when tune is called right after. But, messages can be + // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and + // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS. + mCaptionEnabled = captionEnabled; + mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK); + } + + public TunerChannel getCurrentChannel() { + return mChannel; + } + + @MainThread + public long getStartPosition() { + return mBufferStartTimeMs; + } + + private String getRecordingPath() { + return Uri.parse(mRecordingId).getPath(); + } + + private Long getDurationForRecording(String recordingId) { + DvrStorageManager storageManager = + new DvrStorageManager(new File(getRecordingPath()), false); + List<BufferManager.TrackFormat> trackFormatList = storageManager.readTrackInfoFiles(false); + if (trackFormatList.isEmpty()) { + trackFormatList = storageManager.readTrackInfoFiles(true); + } + if (!trackFormatList.isEmpty()) { + BufferManager.TrackFormat trackFormat = trackFormatList.get(0); + Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION); + // we need duration by milli for trickplay notification. + return durationUs != null ? durationUs / 1000 : null; + } + Log.e(TAG, "meta file for recording was not found: " + recordingId); + return null; + } + + @MainThread + public long getCurrentPosition() { + // TODO: More precise time may be necessary. + MpegTsPlayer mpegTsPlayer = mPlayer; + long currentTime = + mpegTsPlayer != null + ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition() + : mRecordStartTimeMs; + if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) { + currentTime = mRecordingDuration + mRecordStartTimeMs; + } + if (DEBUG) { + long systemCurrentTime = System.currentTimeMillis(); + Log.d( + TAG, + "currentTime = " + + currentTime + + " ; System.currentTimeMillis() = " + + systemCurrentTime + + " ; diff = " + + (currentTime - systemCurrentTime)); + } + return currentTime; + } + + @AnyThread + public void sendMessage(int messageType) { + mHandler.sendEmptyMessage(messageType); + } + + @AnyThread + public void sendMessage(int messageType, Object object) { + mHandler.obtainMessage(messageType, object).sendToTarget(); + } + + @AnyThread + public void sendMessage(int messageType, int arg1, int arg2, Object object) { + mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget(); + } + + @MainThread + public void release() { + if (DEBUG) { + Log.d(TAG, "release()"); + } + synchronized (mReleaseLock) { + mReleaseRequested = true; + } + if (mHasSoftwareAudioDecoder) { + // TODO reimplement for google3 + // Here disconnect ffmpeg + } + mAudioCapabilitiesReceiver.unregister(); + mChannelDataManager.setListener(null); + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MSG_RELEASE); + } + + // MpegTsPlayer.Listener + // Called in the same thread as mHandler. + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady); + } + if (playbackState == mPlayerState) { + return; + } + mReadyStartTimeMs = INVALID_TIME; + mPreparingStartTimeMs = INVALID_TIME; + mBufferingStartTimeMs = INVALID_TIME; + if (playbackState == ExoPlayer.STATE_READY) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer ready"); + } + if (!mPlayerStarted) { + sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer)); + } + mReadyStartTimeMs = SystemClock.elapsedRealtime(); + } else if (playbackState == ExoPlayer.STATE_PREPARING) { + mPreparingStartTimeMs = SystemClock.elapsedRealtime(); + } else if (playbackState == ExoPlayer.STATE_BUFFERING) { + mBufferingStartTimeMs = SystemClock.elapsedRealtime(); + } else if (playbackState == ExoPlayer.STATE_ENDED) { + // Final status + // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards. + Log.i(TAG, "Player ended: end of stream"); + if (mChannel != null) { + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + } + } + mPlayerState = playbackState; + } + + @Override + public void onError(Exception e) { + if (TunerPreferences.getStoreTsStream(mContext)) { + // Crash intentionally to capture the error causing TS file. + Log.e( + TAG, + "Crash intentionally to capture the error causing TS file. " + e.getMessage()); + SoftPreconditions.checkState(false); + } + // There maybe some errors that finally raise ExoPlaybackException and will be handled here. + // If we are playing live stream, retrying playback maybe helpful. But for recorded stream, + // retrying playback is not helpful. + if (mChannel != null) { + mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)) + .sendToTarget(); + } + } + + @Override + public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) { + if (mChannel != null && mChannel.hasVideo()) { + updateVideoTrack(width, height); + } + if (mRecordingId != null) { + updateVideoTrack(width, height); + } + } + + @Override + public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { + if (mSurface != null && mPlayerStarted) { + if (DEBUG) { + Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); + } + if (mRecordingId != null) { + // Workaround of b/33298048: set it to 1 instead of 0. + mBufferStartTimeMs = mRecordStartTimeMs = 1; + } else { + mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); + } + notifyVideoAvailable(); + mReportedDrawnToSurface = true; + + // If surface is drawn successfully, it means that the playback was brought back + // to normal and therefore, the playback recovery status will be reset through + // setting a zero value to the retry count. + // TODO: Consider audio only channels for detecting playback status changes to + // be normal. + mRetryCount = 0; + if (mCaptionEnabled && mCaptionTrack != null) { + startCaptionTrack(); + } else { + stopCaptionTrack(); + } + mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); + } + } + + @Override + public void onSmoothTrickplayForceStopped() { + if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) { + return; + } + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + doTrickplayBySeek((int) mPlayer.getCurrentPosition()); + } + + @Override + public void onAudioUnplayable() { + if (mPlayer == null) { + return; + } + Log.i(TAG, "AC3 audio cannot be played due to device limitation"); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE); + } + + // MpegTsPlayer.VideoEventListener + @Override + public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event); + } + + @Override + public void onClearCaptionEvent() { + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER); + } + + @Override + public void onDiscoverCaptionServiceNumber(int serviceNumber) { + sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber); + } + + // ChannelDataManager.ProgramInfoListener + @Override + public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) { + sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs)); + } + + @Override + public void onChannelArrived(TunerChannel channel) { + sendMessage(MSG_UPDATE_CHANNEL_INFO, channel); + } + + @Override + public void onRescanNeeded() { + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED); + } + + @Override + public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) { + sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs)); + } + + // PlaybackBufferListener + @Override + public void onBufferStartTimeChanged(long startTimeMs) { + sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs); + } + + @Override + public void onBufferStateChanged(boolean available) { + sendMessage(MSG_BUFFER_STATE_CHANGED, available); + } + + @Override + public void onDiskTooSlow() { + mTrickplayDisabledByStorageIssue = true; + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + } + + // EventDetector.EventListener + @Override + public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { + mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); + } + + @Override + public void onEventDetected(TunerChannel channel, List<EitItem> items) { + mChannelDataManager.notifyEventDetected(channel, items); + } + + @Override + public void onChannelScanDone() { + // do nothing. + } + + private long parseChannel(Uri uri) { + try { + List<String> paths = uri.getPathSegments(); + if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) { + return ContentUris.parseId(uri); + } + } catch (UnsupportedOperationException | NumberFormatException e) { + } + return -1; + } + + private static class RecordedProgram { + // private final long mChannelId; + private final String mDataUri; + private final long mStartTimeMillis; + + private static final String[] PROJECTION = { + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, + TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + }; + + public RecordedProgram(Cursor cursor) { + int index = 0; + // mChannelId = cursor.getLong(index++); + index++; + mDataUri = cursor.getString(index++); + mStartTimeMillis = cursor.getLong(index++); + } + + public RecordedProgram(long channelId, String dataUri) { + // mChannelId = channelId; + mDataUri = dataUri; + mStartTimeMillis = 0; + } + + public static RecordedProgram onQuery(Cursor c) { + RecordedProgram recording = null; + if (c != null && c.moveToNext()) { + recording = new RecordedProgram(c); + } + return recording; + } + + public String getDataUri() { + return mDataUri; + } + + public long getStartTime() { + return mStartTimeMillis; + } + } + + private RecordedProgram getRecordedProgram(Uri recordedUri) { + ContentResolver resolver = mContext.getContentResolver(); + try (Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) { + if (c != null) { + RecordedProgram result = RecordedProgram.onQuery(c); + if (DEBUG) { + Log.d(TAG, "Finished query for " + this); + } + return result; + } else { + if (c == null) { + Log.e(TAG, "Unknown query error for " + this); + } else { + if (DEBUG) { + Log.d(TAG, "Canceled query for " + this); + } + } + return null; + } + } + } + + private String parseRecording(Uri uri) { + RecordedProgram recording = getRecordedProgram(uri); + if (recording != null) { + mRecordedProgramStartTimeMs = recording.getStartTime(); + return recording.getDataUri(); + } + return null; + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_TUNE: + return handleMessageTune((Uri) msg.obj); + case MSG_STOP_TUNE: + return handleMessageStopTune(); + case MSG_RELEASE: + return handleMessageRelease(); + case MSG_RETRY_PLAYBACK: + return handleMessageRetryPlayback((int) msg.obj); + case MSG_RESET_PLAYBACK: + return handleMessageResetPlayback(); + case MSG_START_PLAYBACK: + return handleMessageStartPlayback((int) msg.obj); + case MSG_UPDATE_PROGRAM: + return handleMessageUpdateProgram((EitItem) msg.obj); + case MSG_SCHEDULE_OF_PROGRAMS: + // TODO: fix the unchecked cast waring. + Pair<TunerChannel, List<EitItem>> pair = + (Pair<TunerChannel, List<EitItem>>) msg.obj; + return handleMessageScheduleOfPrograms(pair); + case MSG_UPDATE_CHANNEL_INFO: + return handleMessageUpdateChannelInfo((TunerChannel) msg.obj); + case MSG_PROGRAM_DATA_RESULT: + return handleMessageProgramDataResult(msg); + case MSG_TRICKPLAY_BY_SEEK: + return handleMessageTrickplayBySeek(msg.arg1); + case MSG_SMOOTH_TRICKPLAY_MONITOR: + return handleMessageSmoothTrickplayMonitor(); + case MSG_RESCHEDULE_PROGRAMS: + return handleMessageReschedulePrograms(); + case MSG_PARENTAL_CONTROLS: + return handleMessageParentalControl(); + case MSG_UNBLOCKED_RATING: + return handleMessageUnblockedRating((TvContentRating) msg.obj); + case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: + return handleMessageDiscoverCaptionServiceNumber((int) msg.obj); + case MSG_SELECT_TRACK: + return handleMessageSelectTrack(msg.arg1, (String) msg.obj); + case MSG_UPDATE_CAPTION_TRACK: + return handleMessageUpdateCaptionTrack(); + case MSG_TIMESHIFT_PAUSE: + return handleMessageTimeshiftPause(); + case MSG_TIMESHIFT_RESUME: + return handleMessageTimeshiftResume(); + case MSG_TIMESHIFT_SEEK_TO: + return handleMessageTimeshiftSeekTo((long) msg.obj); + case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: + return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj); + case MSG_SET_STREAM_VOLUME: + return handleMessageSetStreamVolume(); + case MSG_TUNER_PREFERENCES_CHANGED: + return handleMessageTunerPreferencesChanged(); + case MSG_BUFFER_START_TIME_CHANGED: + return handleMessageBufferStartTimeChanged((long) msg.obj); + case MSG_BUFFER_STATE_CHANGED: + return handleMessageBufferStateChanged((boolean) msg.obj); + case MSG_CHECK_SIGNAL: + return handleMessageCheckSignal(); + case MSG_SET_SURFACE: + return handleMessageSetSurface(); + case MSG_NOTIFY_AUDIO_TRACK_UPDATED: + return handleMessageAudioTrackUpdated(); + case MSG_CHECK_SIGNAL_STRENGTH: + return handleMessageCheckSignalStrength(); + default: + return unhandledMessage(msg); + } + } + + private boolean handleMessageTune(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "MSG_TUNE"); + } + + // When sequential tuning messages arrived, it skips middle tuning messages in + // order + // to change to the last requested channel quickly. + if (mHandler.hasMessages(MSG_TUNE)) { + return true; + } + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); + if (!mIsActiveSession) { + // Wait until release is finished if there is a pending release. + try { + while (!sActiveSessionSemaphore.tryAcquire( + RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { + synchronized (mReleaseLock) { + if (mReleaseRequested) { + return true; + } + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + synchronized (mReleaseLock) { + if (mReleaseRequested) { + sActiveSessionSemaphore.release(); + return true; + } + } + mIsActiveSession = true; + } + String recording = null; + long channelId = parseChannel(channelUri); + TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId); + if (channelId == -1) { + recording = parseRecording(channelUri); + } + if (channel == null && recording == null) { + Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); + stopTune(); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + return true; + } + clearCallbacksAndMessagesSafely(); + mChannelDataManager.removeAllCallbacksAndMessages(); + if (channel != null) { + mChannelDataManager.requestProgramsData(channel); + } + prepareTune(channel, recording); + // TODO: Need to refactor. notifyContentAllowed() should not be called if + // parental + // control is turned on. + mSession.notifyContentAllowed(); + resetTvTracks(); + resetPlayback(); + mHandler.sendEmptyMessageDelayed( + MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); + return true; + } + + private boolean handleMessageStopTune() { + if (DEBUG) { + Log.d(TAG, "MSG_STOP_TUNE"); + } + mChannel = null; + stopPlayback(true); + stopCaptionTrack(); + resetTvTracks(); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + return true; + } + + private boolean handleMessageRelease() { + if (DEBUG) { + Log.d(TAG, "MSG_RELEASE"); + } + mHandler.removeCallbacksAndMessages(null); + stopPlayback(true); + stopCaptionTrack(); + mSourceManager.release(); + mHandler.getLooper().quitSafely(); + if (mIsActiveSession) { + sActiveSessionSemaphore.release(); + } + return true; + } + + private boolean handleMessageRetryPlayback(int code) { + if (System.identityHashCode(mPlayer) == code) { + Log.i(TAG, "Retrying the playback for channel: " + mChannel); + mHandler.removeMessages(MSG_RETRY_PLAYBACK); + // When there is a request of retrying playback, don't reuse TunerHal. + mSourceManager.setKeepTuneStatus(false); + mRetryCount++; + if (DEBUG) { + Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); + } + mChannelDataManager.removeAllCallbacksAndMessages(); + if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { + resetPlayback(); + } else { + // When it reaches this point, it may be due to an error that occurred + // in + // the tuner device. Calling stopPlayback() resets the tuner device + // to recover from the error. + stopPlayback(false); + stopCaptionTrack(); + + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + Log.i(TAG, "Notify weak signal since fail to retry playback"); + + // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically + // chosen + // value before recovering the playback. + mHandler.sendEmptyMessageDelayed( + MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS); + } + } + return true; + } + + private boolean handleMessageResetPlayback() { + if (DEBUG) { + Log.d(TAG, "MSG_RESET_PLAYBACK"); + } + mChannelDataManager.removeAllCallbacksAndMessages(); + resetPlayback(); + return true; + } + + private boolean handleMessageStartPlayback(int playerHashCode) { + if (DEBUG) { + Log.d(TAG, "MSG_START_PLAYBACK"); + } + if (mChannel != null || mRecordingId != null) { + startPlayback(playerHashCode); + } + return true; + } + + private boolean handleMessageUpdateProgram(EitItem program) { + if (mChannel != null) { + updateTvTracks(program, false); + mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); + } + return true; + } + + private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) { + mHandler.removeMessages(MSG_UPDATE_PROGRAM); + TunerChannel channel = pair.first; + if (mChannel == null) { + return true; + } + if (mChannel != null && mChannel.compareTo(channel) != 0) { + return true; + } + mPrograms = pair.second; + EitItem currentProgram = getCurrentProgram(); + if (currentProgram == null) { + mProgram = null; + } + long currentTimeMs = getCurrentPosition(); + if (mPrograms != null) { + for (EitItem item : mPrograms) { + if (currentProgram != null && currentProgram.compareTo(item) == 0) { + if (DEBUG) { + Log.d(TAG, "Update current TvTracks " + item); + } + if (mProgram != null && mProgram.compareTo(item) == 0) { + continue; + } + mProgram = item; + updateTvTracks(item, false); + } else if (item.getStartTimeUtcMillis() > currentTimeMs) { + if (DEBUG) { + Log.d( + TAG, + "Update next TvTracks " + + item + + " " + + (item.getStartTimeUtcMillis() - currentTimeMs)); + } + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item), + item.getStartTimeUtcMillis() - currentTimeMs); + } + } + } + mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); + return true; + } + + private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) { + if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) { + updateChannelInfo(tunerChannel); + } + return true; + } + + private boolean handleMessageProgramDataResult(Message msg) { + TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first; + + // If there already exists, skip it since real-time data is a top priority, + if (mChannel != null + && mChannel.compareTo(channel) == 0 + && mPrograms == null + && mProgram == null) { + sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj); + } + return true; + } + + private boolean handleMessageTrickplayBySeek(int seekPositionMs) { + if (mPlayer == null) { + return true; + } + if (mRecordingId != null) { + long systemBufferTime = + System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs; + if (seekPositionMs > systemBufferTime) { + doTimeShiftResume(); + return true; + } + } + doTrickplayBySeek(seekPositionMs); + return true; + } + + private boolean handleMessageSmoothTrickplayMonitor() { + if (mPlayer == null) { + return true; + } + long systemCurrentTime = System.currentTimeMillis(); + long position = getCurrentPosition(); + if (mRecordingId == null) { + // Checks if the position exceeds the upper bound when forwarding, + // or exceed the lower bound when rewinding. + // If the direction is not checked, there can be some issues. + // (See b/29939781 for more details.) + if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L) + || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) { + doTimeShiftResume(); + return true; + } + } else { + if (position > mRecordingDuration || position < 0) { + doTimeShiftPause(); + return true; + } + long systemBufferTime = + systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs; + if (position > systemBufferTime) { + doTimeShiftResume(); + return true; + } + } + mHandler.sendEmptyMessageDelayed( + MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); + return true; + } + + private boolean handleMessageReschedulePrograms() { + if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { + mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); + } else { + doReschedulePrograms(); + } + return true; + } + + private boolean handleMessageParentalControl() { + doParentalControls(); + mHandler.removeMessages(MSG_PARENTAL_CONTROLS); + mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); + return true; + } + + private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) { + mUnblockedContentRating = unblockedContentRating; + return handleMessageParentalControl(); + } + + private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) { + doDiscoverCaptionServiceNumber(serviceNumber); + return true; + } + + private boolean handleMessageSelectTrack(int type, String trackId) { + if (mPlayer == null) { + Log.w(TAG, "mPlayer is null when doselectTrack is called"); + return false; + } + if (mChannel != null || mRecordingId != null) { + doSelectTrack(type, trackId); + } + return true; + } + + private boolean handleMessageUpdateCaptionTrack() { + if (mCaptionEnabled) { + startCaptionTrack(); + } else { + stopCaptionTrack(); + } + return true; + } + + private boolean handleMessageTimeshiftPause() { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftPause(); + return true; + } + + private boolean handleMessageTimeshiftResume() { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_RESUME"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftResume(); + return true; + } + + private boolean handleMessageTimeshiftSeekTo(long timeMs) { + if (DEBUG) { + Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")"); + } + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftSeekTo(timeMs); + return true; + } + + private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) { + if (mPlayer == null) { + return true; + } + setTrickplayEnabledIfNeeded(); + doTimeShiftSetPlaybackParams(playbackParams); + return true; + } + + private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + if (DEBUG) { + Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities); + } + if (audioCapabilities == null) { + return true; + } + if (!audioCapabilities.equals(mAudioCapabilities)) { + // HDMI supported encodings are changed. restart player. + mAudioCapabilities = audioCapabilities; + resetPlayback(); + } + return true; + } + + private boolean handleMessageSetStreamVolume() { + if (mPlayer != null && mPlayer.isPlaying()) { + mPlayer.setVolume(mVolume); + } + return true; + } + + private boolean handleMessageTunerPreferencesChanged() { + mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED); + @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext); + if (trickplaySetting != mTrickplaySetting) { + boolean wasTrcikplayEnabled = + mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; + boolean isTrickplayEnabled = + trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; + mTrickplaySetting = trickplaySetting; + if (isTrickplayEnabled != wasTrcikplayEnabled) { + sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer)); + } + } + return true; + } + + private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) { + if (mPlayer == null) { + return true; + } + mBufferStartTimeMs = bufferStartTimeMs; + if (!hasEnoughBackwardBuffer() + && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) { + mPlayer.setPlayWhenReady(true); + mPlayer.setAudioTrackAndClosedCaption(true); + mPlaybackParams.setSpeed(1.0f); + } + return true; + } + + private boolean handleMessageBufferStateChanged(boolean available) { + mSession.notifyTimeShiftStatusChanged( + available + ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE + : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); + return true; + } + + private boolean handleMessageCheckSignal() { + if (mChannel == null || mPlayer == null) { + return true; + } + TsDataSource source = mPlayer.getDataSource(); + long limitInBytes = source != null ? source.getBufferedPosition() : 0L; + if (TunerDebug.ENABLED) { + TunerDebug.calculateDiff(); + mTunerSessionOverlay.sendUiMessage( + TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT, + Html.fromHtml( + StatusTextUtils.getStatusWarningInHTML( + (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, + TunerDebug.getVideoFrameDrop(), + TunerDebug.getBytesInQueue(), + TunerDebug.getAudioPositionUs(), + TunerDebug.getAudioPositionUsRate(), + TunerDebug.getAudioPtsUs(), + TunerDebug.getAudioPtsUsRate(), + TunerDebug.getVideoPtsUs(), + TunerDebug.getVideoPtsUsRate()))); + } + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE); + long currentTime = SystemClock.elapsedRealtime(); + long bufferingTimeMs = + mBufferingStartTimeMs != INVALID_TIME + ? currentTime - mBufferingStartTimeMs + : mBufferingStartTimeMs; + long preparingTimeMs = + mPreparingStartTimeMs != INVALID_TIME + ? currentTime - mPreparingStartTimeMs + : mPreparingStartTimeMs; + boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + boolean isWeakSignal = + source != null + && mChannel.getType() != Channel.TunerType.TYPE_FILE + && (isBufferingTooLong || isPreparingTooLong); + if (isWeakSignal && !mReportedWeakSignal) { + if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), + PLAYBACK_RETRY_DELAY_MS); + } + if (mPlayer != null) { + mPlayer.setAudioTrackAndClosedCaption(false); + } + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mSession.notifySignalStrength(0); + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); + } + Log.i( + TAG, + "Notify weak signal due to signal check, " + + String.format( + "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " + + "videoFrameDrop:%d", + (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, + bufferingTimeMs, + preparingTimeMs, + TunerDebug.getVideoFrameDrop())); + } else if (!isWeakSignal && mReportedWeakSignal) { + boolean isPlaybackStable = + mReadyStartTimeMs != INVALID_TIME + && currentTime - mReadyStartTimeMs + > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + if (!isPlaybackStable) { + // Wait until playback becomes stable. + } else if (mReportedDrawnToSurface) { + mHandler.removeMessages(MSG_RETRY_PLAYBACK); + notifyVideoAvailable(); + mPlayer.setAudioTrackAndClosedCaption(true); + if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) { + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); + } + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + sendMessage(MSG_CHECK_SIGNAL_STRENGTH); + } + } + } + mLastLimitInBytes = limitInBytes; + mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); + return true; + } + + private boolean handleMessageSetSurface() { + if (mPlayer != null) { + mPlayer.setSurface(mSurface); + } else { + // TODO: Since surface is dynamically set, we can remove the dependency of + // playback start on mSurface nullity. + resetPlayback(); + } + return true; + } + + private boolean handleMessageAudioTrackUpdated() { + notifyAudioTracksUpdated(); + return true; + } + + private boolean handleMessageCheckSignalStrength() { + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + int signal; + if (mPlayer != null) { + TsDataSource source = mPlayer.getDataSource(); + if (source != null) { + signal = source.getSignalStrength(); + return handleSignal(signal); + } + } + } + return false; + } + + @VisibleForTesting + protected boolean handleSignal(int signal) { + if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED + || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) { + notifySignal(signal); + return true; + } + if (signal != mSignalStrength && signal >= 0) { + notifySignal(signal); + } + mHandler.sendEmptyMessageDelayed( + MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS); + return true; + } + + @VisibleForTesting + protected void notifySignal(int signal) { + mSession.notifySignalStrength(signal); + mSignalStrength = signal; + } + + private boolean unhandledMessage(Message msg) { + Log.w(TAG, "Unhandled message code: " + msg.what); + return false; + } + + // Private methods + private void doSelectTrack(int type, String trackId) { + int numTrackId = + trackId != null ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1; + if (type == TvTrackInfo.TYPE_AUDIO) { + if (trackId == null) { + return; + } + if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) { + mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId); + } + mSession.notifyTrackSelected(type, trackId); + } else if (type == TvTrackInfo.TYPE_SUBTITLE) { + if (trackId == null) { + mSession.notifyTrackSelected(type, null); + mCaptionTrack = null; + stopCaptionTrack(); + return; + } + for (TvTrackInfo track : mTvTracks) { + if (track.getId().equals(trackId)) { + // The service number of the caption service is used for track id of a + // subtitle track. Passes the following track id on to TsParser. + mSession.notifyTrackSelected(type, trackId); + mCaptionTrack = mCaptionTrackMap.get(numTrackId); + startCaptionTrack(); + return; + } + } + } + } + + private void setTrickplayEnabledIfNeeded() { + if (mChannel == null + || mTrickplayModeCustomization != CustomizationManager.TRICKPLAY_MODE_ENABLED) { + return; + } + if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { + mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED; + TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting); + } + } + + @VisibleForTesting + protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) { + if (capabilities == null) { + Log.w(TAG, "No Audio Capabilities"); + } + mSourceManager.setKeepTuneStatus(true); + long now = System.currentTimeMillis(); + if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED + && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { + if (mTrickplayExpiredMs == 0) { + mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS; + TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs); + } else { + if (mTrickplayExpiredMs < now) { + mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED; + TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting); + } + } + } + BufferManager bufferManager = null; + if (mRecordingId != null) { + StorageManager storageManager = + new DvrStorageManager(new File(getRecordingPath()), false); + bufferManager = new BufferManager(storageManager); + updateCaptionTracks(((DvrStorageManager) storageManager).readCaptionInfoFiles()); + } else if (!mTrickplayDisabledByStorageIssue + && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED + && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { + bufferManager = + new BufferManager( + new TrickplayStorageManager( + mContext, + mTrickplayBufferDir, + 1024L * 1024 * mMaxTrickplayBufferSizeMb)); + } else { + Log.w(TAG, "Trickplay is disabled."); + } + MpegTsPlayer player = + new MpegTsPlayer( + new MpegTsRendererBuilder( + mContext, bufferManager, this, mConcurrentDvrPlaybackFlags), + mHandler, + mSourceManager, + capabilities, + this); + Log.i(TAG, "Passthrough AC3 renderer"); + if (DEBUG) { + Log.d(TAG, "ExoPlayer created"); + } + player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); + player.setVideoEventListener(this); + player.setCaptionServiceNumber( + mCaptionTrack != null + ? mCaptionTrack.serviceNumber + : Cea708Data.EMPTY_SERVICE_NUMBER); + return player; + } + + private void startCaptionTrack() { + if (mCaptionEnabled && mCaptionTrack != null) { + mTunerSessionOverlay.sendUiMessage( + TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); + if (mPlayer != null) { + mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber); + } + } + } + + private void stopCaptionTrack() { + if (mPlayer != null) { + mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); + } + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK); + } + + private void resetTvTracks() { + mTvTracks.clear(); + mAudioTrackMap.clear(); + mCaptionTrackMap.clear(); + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK); + mSession.notifyTracksChanged(mTvTracks); + } + + private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) { + synchronized (tvTracksInterface) { + if (DEBUG) { + Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); + } + List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks(); + List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks(); + // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for + // audio + // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust + // audio + // track info in PMT more and use info in EIT only when we have nothing. + if (audioTracks != null + && !audioTracks.isEmpty() + && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) { + updateAudioTracks(audioTracks); + } + if (captionTracks == null || captionTracks.isEmpty()) { + if (tvTracksInterface.hasCaptionTrack()) { + updateCaptionTracks(captionTracks); + } + } else { + updateCaptionTracks(captionTracks); + } + } + } + + private void removeTvTracks(int trackType) { + Iterator<TvTrackInfo> iterator = mTvTracks.iterator(); + while (iterator.hasNext()) { + TvTrackInfo tvTrackInfo = iterator.next(); + if (tvTrackInfo.getType() == trackType) { + iterator.remove(); + } + } + } + + private void updateVideoTrack(int width, int height) { + removeTvTracks(TvTrackInfo.TYPE_VIDEO); + mTvTracks.add( + new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID) + .setVideoWidth(width) + .setVideoHeight(height) + .build()); + mSession.notifyTracksChanged(mTvTracks); + mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID); + } + + private void updateAudioTracks(List<AtscAudioTrack> audioTracks) { + if (DEBUG) { + Log.d(TAG, "Update AudioTracks " + audioTracks); + } + mAudioTrackMap.clear(); + if (audioTracks != null) { + int index = 0; + for (AtscAudioTrack audioTrack : audioTracks) { + audioTrack.index = index; + mAudioTrackMap.put(index, audioTrack); + ++index; + } + } + mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); + } + + private void notifyAudioTracksUpdated() { + if (mPlayer == null) { + // Audio tracks will be updated later once player initialization is done. + return; + } + int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); + removeTvTracks(TvTrackInfo.TYPE_AUDIO); + for (int i = 0; i < audioTrackCount; i++) { + // We use language information from EIT/VCT only when the player does not provide + // languages. + com.google.android.exoplayer.MediaFormat infoFromPlayer = + mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i); + AtscAudioTrack infoFromEit = mAudioTrackMap.get(i); + AtscAudioTrack infoFromVct = + (mChannel != null + && mChannel.getAudioTracks().size() == mAudioTrackMap.size() + && i < mChannel.getAudioTracks().size()) + ? mChannel.getAudioTracks().get(i) + : null; + String language = + !TextUtils.isEmpty(infoFromPlayer.language) + ? infoFromPlayer.language + : (infoFromEit != null && infoFromEit.language != null) + ? infoFromEit.language + : (infoFromVct != null && infoFromVct.language != null) + ? infoFromVct.language + : null; + TvTrackInfo.Builder builder = + new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i); + builder.setLanguage(language); + builder.setAudioChannelCount(infoFromPlayer.channelCount); + builder.setAudioSampleRate(infoFromPlayer.sampleRate); + TvTrackInfo track = builder.build(); + mTvTracks.add(track); + } + mSession.notifyTracksChanged(mTvTracks); + } + + private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) { + if (DEBUG) { + Log.d(TAG, "Update CaptionTrack " + captionTracks); + } + removeTvTracks(TvTrackInfo.TYPE_SUBTITLE); + mCaptionTrackMap.clear(); + if (captionTracks != null) { + for (AtscCaptionTrack captionTrack : captionTracks) { + if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) { + continue; + } + String language = captionTrack.language; + + // The service number of the caption service is used for track id of a subtitle. + // Later, when a subtitle is chosen, track id will be passed on to TsParser. + TvTrackInfo.Builder builder = + new TvTrackInfo.Builder( + TvTrackInfo.TYPE_SUBTITLE, + SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber); + builder.setLanguage(language); + mTvTracks.add(builder.build()); + mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack); + } + } + mSession.notifyTracksChanged(mTvTracks); + } + + private void updateChannelInfo(TunerChannel channel) { + if (DEBUG) { + Log.d( + TAG, + String.format( + "Channel Info (old) videoPid: %d audioPid: %d " + "audioSize: %d", + mChannel.getVideoPid(), + mChannel.getAudioPid(), + mChannel.getAudioPids().size())); + } + + // The list of the audio tracks resided in a channel is often changed depending on a + // program being on the air. So, we should update the streaming PIDs and types of the + // tuned channel according to the newly received channel data. + int oldVideoPid = mChannel.getVideoPid(); + int oldAudioPid = mChannel.getAudioPid(); + List<Integer> audioPids = channel.getAudioPids(); + List<Integer> audioStreamTypes = channel.getAudioStreamTypes(); + int size = audioPids.size(); + mChannel.setVideoPid(channel.getVideoPid()); + mChannel.setAudioPids(audioPids); + mChannel.setAudioStreamTypes(audioStreamTypes); + updateTvTracks(channel, true); + int index = audioPids.isEmpty() ? -1 : 0; + for (int i = 0; i < size; ++i) { + if (audioPids.get(i) == oldAudioPid) { + index = i; + break; + } + } + mChannel.selectAudioTrack(index); + mSession.notifyTrackSelected( + TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index); + + // Reset playback if there is a change in the listening streaming PIDs. + if (oldVideoPid != mChannel.getVideoPid() || oldAudioPid != mChannel.getAudioPid()) { + // TODO: Implement a switching between tracks more smoothly. + resetPlayback(); + } + if (DEBUG) { + Log.d( + TAG, + String.format( + "Channel Info (new) videoPid: %d audioPid: %d " + " audioSize: %d", + mChannel.getVideoPid(), + mChannel.getAudioPid(), + mChannel.getAudioPids().size())); + } + } + + private void stopPlayback(boolean removeChannelDataCallbacks) { + if (removeChannelDataCallbacks) { + mChannelDataManager.removeAllCallbacksAndMessages(); + } + if (mPlayer != null) { + mPlayer.setPlayWhenReady(false); + mPlayer.release(); + mPlayer = null; + mPlayerState = ExoPlayer.STATE_IDLE; + mPlaybackParams.setSpeed(1.0f); + mPlayerStarted = false; + mReportedDrawnToSurface = false; + mPreparingStartTimeMs = INVALID_TIME; + mBufferingStartTimeMs = INVALID_TIME; + mReadyStartTimeMs = INVALID_TIME; + mLastLimitInBytes = 0L; + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE); + mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); + } + } + + private void startPlayback(int playerHashCode) { + // TODO: provide hasAudio()/hasVideo() for play recordings. + if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) { + return; + } + if (mChannel != null && !mChannel.hasAudio()) { + if (DEBUG) { + Log.d(TAG, "Channel " + mChannel + " does not have audio."); + } + // Playbacks with video-only stream have not been tested yet. + // No video-only channel has been found. + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + return; + } + if (mChannel != null + && ((mChannel.hasAudio() && !mPlayer.hasAudio()) + || (mChannel.hasVideo() && !mPlayer.hasVideo())) + && mChannel.getType() != Channel.TunerType.TYPE_NETWORK) { + // If the channel is from network, skip this part since the video and audio tracks + // information for channels from network are more reliable in the extractor. Otherwise, + // tracks haven't been detected in the extractor. Try again. + sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); + return; + } + // Since mSurface is volatile, we define a local variable surface to keep the same value + // inside this method. + Surface surface = mSurface; + if (surface != null && !mPlayerStarted) { + mPlayer.setSurface(surface); + mPlayer.setPlayWhenReady(true); + mPlayer.setVolume(mVolume); + if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) { + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); + } else if (!mReportedWeakSignal) { + // Doesn't show buffering during weak signal. + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); + } + mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE); + mPlayerStarted = true; + } + } + + @VisibleForTesting + protected void preparePlayback() { + MpegTsPlayer player = createPlayer(mAudioCapabilities); + if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) { + mSourceManager.setKeepTuneStatus(false); + player.release(); + if (!mHandler.hasMessages(MSG_TUNE)) { + // When prepare failed, there may be some errors related to hardware. In that + // case, retry playback immediately may not help. + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + Log.i(TAG, "Notify weak signal due to player preparation failure"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), + PLAYBACK_RETRY_DELAY_MS); + } + } else { + mPlayer = player; + mPlayerStarted = false; + mHandler.removeMessages(MSG_CHECK_SIGNAL); + mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); + if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) { + mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH); + } + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH); + } + } + } + + private void resetPlayback() { + long timestamp; + long oldTimestamp; + timestamp = SystemClock.elapsedRealtime(); + stopPlayback(false); + stopCaptionTrack(); + if (ENABLE_PROFILER) { + oldTimestamp = timestamp; + timestamp = SystemClock.elapsedRealtime(); + Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms"); + } + if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) { + return; + } + SoftPreconditions.checkState(mPlayer == null); + preparePlayback(); + } + + private void prepareTune(TunerChannel channel, String recording) { + mChannelBlocked = false; + mUnblockedContentRating = null; + mRetryCount = 0; + mChannel = channel; + mRecordingId = recording; + mRecordingDuration = recording != null ? getDurationForRecording(recording) : null; + mProgram = null; + mPrograms = null; + if (mRecordingId != null) { + // Workaround of b/33298048: set it to 1 instead of 0. + mBufferStartTimeMs = mRecordStartTimeMs = 1; + } else { + mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); + } + mLastPositionMs = 0; + mCaptionTrack = null; + mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN; + if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) { + mSession.notifySignalStrength(mSignalStrength); + } + mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); + } + + private void doReschedulePrograms() { + long currentPositionMs = getCurrentPosition(); + long forwardDifference = + Math.abs(currentPositionMs - mLastPositionMs - RESCHEDULE_PROGRAMS_INTERVAL_MS); + mLastPositionMs = currentPositionMs; + + // A gap is measured as the time difference between previous and next current position + // periodically. If the gap has a significant difference with an interval of a period, + // this means that there is a change of playback status and the programs of the current + // channel should be rescheduled to new playback timeline. + if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) { + if (DEBUG) { + Log.d( + TAG, + "reschedule programs size:" + + (mPrograms != null ? mPrograms.size() : 0) + + " current program: " + + getCurrentProgram()); + } + mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms)) + .sendToTarget(); + } + mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS); + mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INTERVAL_MS); + } + + private int getTrickPlaySeekIntervalMs() { + return Math.max( + EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()), + MIN_TRICKPLAY_SEEK_INTERVAL_MS); + } + + @SuppressWarnings("NarrowingCompoundAssignment") + private void doTrickplayBySeek(int seekPositionMs) { + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) { + return; + } + if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) { + if (mPlaybackParams.getSpeed() > 1.0f) { + // If fast forwarding, the seekPositionMs can be out of the buffered range + // because of chuck evictions. + seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs); + } else { + mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs); + mPlaybackParams.setSpeed(1.0f); + mPlayer.setAudioTrackAndClosedCaption(true); + return; + } + } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) { + // Stops trickplay when FF requested the position later than current position. + // If RW trickplay requested the position later than current position, + // continue trickplay. + if (mPlaybackParams.getSpeed() > 0.0f) { + mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs); + mPlaybackParams.setSpeed(1.0f); + mPlayer.setAudioTrackAndClosedCaption(true); + return; + } + } + + long delayForNextSeek = getTrickPlaySeekIntervalMs(); + if (!mPlayer.isBuffering()) { + mPlayer.seekTo(seekPositionMs); + } else { + delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS; + } + seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek; + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek); + } + + private void doTimeShiftPause() { + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + if (!hasEnoughBackwardBuffer()) { + return; + } + mPlaybackParams.setSpeed(1.0f); + mPlayer.setPlayWhenReady(false); + mPlayer.setAudioTrackAndClosedCaption(true); + } + + private void doTimeShiftResume() { + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + mPlaybackParams.setSpeed(1.0f); + mPlayer.setPlayWhenReady(true); + mPlayer.setAudioTrackAndClosedCaption(true); + } + + private void doTimeShiftSeekTo(long timeMs) { + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs)); + } + + private void doTimeShiftSetPlaybackParams(PlaybackParams params) { + if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) { + return; + } + mPlaybackParams = params; + float speed = mPlaybackParams.getSpeed(); + if (speed == 1.0f) { + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + doTimeShiftResume(); + } else if (mPlayer.supportSmoothTrickPlay(speed)) { + mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); + mPlayer.setAudioTrackAndClosedCaption(false); + mPlayer.startSmoothTrickplay(mPlaybackParams); + mHandler.sendEmptyMessageDelayed( + MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); + } else { + mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); + if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) { + mPlayer.setAudioTrackAndClosedCaption(false); + mPlayer.setPlayWhenReady(false); + // Initiate trickplay + mHandler.sendMessage( + mHandler.obtainMessage( + MSG_TRICKPLAY_BY_SEEK, + (int) + (mPlayer.getCurrentPosition() + + speed * getTrickPlaySeekIntervalMs()), + 0)); + } + } + } + + private EitItem getCurrentProgram() { + if (mPrograms == null || mPrograms.isEmpty()) { + return null; + } + if (mChannel.getType() == Channel.TunerType.TYPE_FILE) { + // For the playback from the local file, we use the first one from the given program. + EitItem first = mPrograms.get(0); + if (first != null + && (mProgram == null + || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) { + return first; + } + return null; + } + long currentTimeMs = getCurrentPosition(); + for (EitItem item : mPrograms) { + if (item.getStartTimeUtcMillis() <= currentTimeMs + && item.getEndTimeUtcMillis() >= currentTimeMs) { + return item; + } + } + return null; + } + + private void doParentalControls() { + boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled(); + if (isParentalControlsEnabled) { + TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked(); + if (DEBUG) { + if (blockContentRating != null) { + Log.d( + TAG, + "Check parental controls: blocked by content rating - " + + blockContentRating); + } else { + Log.d(TAG, "Check parental controls: available"); + } + } + updateChannelBlockStatus(blockContentRating != null, blockContentRating); + } else { + if (DEBUG) { + Log.d(TAG, "Check parental controls: available"); + } + updateChannelBlockStatus(false, null); + } + } + + private void doDiscoverCaptionServiceNumber(int serviceNumber) { + int index = mCaptionTrackMap.indexOfKey(serviceNumber); + if (index < 0) { + AtscCaptionTrack captionTrack = new AtscCaptionTrack(); + captionTrack.serviceNumber = serviceNumber; + captionTrack.wideAspectRatio = false; + captionTrack.easyReader = false; + mCaptionTrackMap.put(serviceNumber, captionTrack); + mTvTracks.add( + new TvTrackInfo.Builder( + TvTrackInfo.TYPE_SUBTITLE, + SUBTITLE_TRACK_PREFIX + serviceNumber) + .build()); + mSession.notifyTracksChanged(mTvTracks); + } + } + + private TvContentRating getContentRatingOfCurrentProgramBlocked() { + EitItem currentProgram = getCurrentProgram(); + if (currentProgram == null) { + return null; + } + ImmutableList<TvContentRating> ratings = + mTvContentRatingCache.getRatings(currentProgram.getContentRating()); + if ((ratings == null || ratings.isEmpty())) { + if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) { + ratings = ImmutableList.of(TvContentRating.UNRATED); + } else { + ratings = NO_CONTENT_RATINGS; + } + } + for (TvContentRating rating : ratings) { + if (!Objects.equals(mUnblockedContentRating, rating) + && mTvInputManager.isRatingBlocked(rating)) { + return rating; + } + } + return null; + } + + private void updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating) { + if (mChannelBlocked == channelBlocked) { + return; + } + mChannelBlocked = channelBlocked; + if (mChannelBlocked) { + clearCallbacksAndMessagesSafely(); + stopPlayback(true); + resetTvTracks(); + if (contentRating != null) { + mSession.notifyContentBlocked(contentRating); + } + mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); + } else { + clearCallbacksAndMessagesSafely(); + resetPlayback(); + mSession.notifyContentAllowed(); + mHandler.sendEmptyMessageDelayed( + MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); + mHandler.removeMessages(MSG_CHECK_SIGNAL); + mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); + } + } + + @WorkerThread + private void clearCallbacksAndMessagesSafely() { + synchronized (mReleaseLock) { + if (!mReleaseRequested) { + // This check prevents removing MSG_RELEASE from the queue, which would prevent this + // session worker from being released. + mHandler.removeCallbacksAndMessages(null); + } + } + } + + private boolean hasEnoughBackwardBuffer() { + return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS + >= mBufferStartTimeMs - mRecordStartTimeMs; + } + + private void notifyVideoUnavailable(final int reason) { + mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + if (mSession != null) { + mSession.notifyVideoUnavailable(reason); + } + } + + private void notifyVideoAvailable() { + mReportedWeakSignal = false; + if (mSession != null) { + mSession.notifyVideoAvailable(); + } + } +} diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java index cdcc00d5..321c7ba9 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java +++ b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java @@ -156,7 +156,9 @@ public class TunerStorageCleanUpService extends JobService { if (lastModified != 0 && lastModified < now - ELAPSED_MILLIS_TO_DELETE) { // To prevent current recordings from being deleted, // deletes recordings which was not modified for long enough time. - CommonUtils.deleteDirOrFile(recordingDir); + if (!CommonUtils.deleteDirOrFile(recordingDir)) { + Log.w(TAG, "Unable to delete recording data at " + recordingDir); + } } } } catch (IOException | SecurityException e) { diff --git a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java index c1d8f278..585b28bc 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.tvinput; +package com.android.tv.tuner.tvinput.datamanager; import android.content.ContentProviderOperation; import android.content.ContentUris; @@ -32,15 +32,15 @@ import android.os.RemoteException; import android.support.annotation.Nullable; import android.text.format.DateUtils; import android.util.Log; -import com.android.tv.common.BaseApplication; +import com.android.tv.common.singletons.HasSingletons; +import com.android.tv.common.singletons.HasTvInputId; import com.android.tv.common.util.PermissionUtils; -import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.PsipData.EitItem; import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.prefs.TunerPreferences; import com.android.tv.tuner.util.ConvertUtils; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -100,7 +100,7 @@ public class ChannelDataManager implements Handler.Callback { private final Context mContext; private final String mInputId; private ProgramInfoListener mListener; - private ChannelScanListener mChannelScanListener; + private ChannelHandlingDoneListener mChannelHandlingDoneListener; private Handler mChannelScanHandler; private final HandlerThread mHandlerThread; private final Handler mHandler; @@ -140,14 +140,15 @@ public class ChannelDataManager implements Handler.Callback { void onRescanNeeded(); } - public interface ChannelScanListener { + /** Listens for all channel handling to be done. */ + public interface ChannelHandlingDoneListener { /** Invoked when all pending channels have been handled. */ void onChannelHandlingDone(); } public ChannelDataManager(Context context) { mContext = context; - mInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); + mInputId = HasSingletons.get(HasTvInputId.class, context).getEmbeddedTunerInputId(); mChannelsUri = TvContract.buildChannelsUriForInput(mInputId); mTunerChannelMap = new ConcurrentHashMap<>(); mTunerChannelIdMap = new ConcurrentSkipListMap<>(); @@ -185,8 +186,8 @@ public class ChannelDataManager implements Handler.Callback { mListener = listener; } - public void setChannelScanListener(ChannelScanListener listener, Handler handler) { - mChannelScanListener = listener; + public void setChannelScanListener(ChannelHandlingDoneListener listener, Handler handler) { + mChannelHandlingDoneListener = listener; mChannelScanHandler = handler; } @@ -198,7 +199,7 @@ public class ChannelDataManager implements Handler.Callback { public void releaseSafely() { mHandlerThread.quitSafely(); mListener = null; - mChannelScanListener = null; + mChannelHandlingDoneListener = null; mChannelScanHandler = null; } @@ -305,16 +306,10 @@ public class ChannelDataManager implements Handler.Callback { Log.e(TAG, "Error deleting obsolete channels", e); } } - if (mChannelScanListener != null && mChannelScanHandler != null) { - mChannelScanHandler.post( - new Runnable() { - @Override - public void run() { - mChannelScanListener.onChannelHandlingDone(); - } - }); + if (mChannelHandlingDoneListener != null && mChannelScanHandler != null) { + mChannelScanHandler.post(() -> mChannelHandlingDoneListener.onChannelHandlingDone()); } else { - Log.e(TAG, "Error. mChannelScanListener is null."); + Log.e(TAG, "Error. mChannelHandlingDoneListener is null."); } } @@ -441,14 +436,10 @@ public class ChannelDataManager implements Handler.Callback { Collections.binarySearch( oldItems, newItem, - new Comparator<EitItem>() { - @Override - public int compare(EitItem lhs, EitItem rhs) { - return Long.compare( + (EitItem lhs, EitItem rhs) -> + Long.compare( lhs.getStartTimeUtcMillis(), - rhs.getStartTimeUtcMillis()); - } - }); + rhs.getStartTimeUtcMillis())); if (pos >= 0) { // Same start Time found. Overlapped. continue; diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java index 1df0b5c3..a92bc59f 100644 --- a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java +++ b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.tvinput; +package com.android.tv.tuner.tvinput.debug; import android.os.SystemClock; import android.util.Log; diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java new file mode 100644 index 00000000..a27cb22a --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java @@ -0,0 +1,25 @@ +package com.android.tv.tuner.tvinput.factory; + +import android.content.Context; +import android.media.tv.TvInputService.Session; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; + +/** {@link android.media.tv.TvInputService.Session} factory */ +public interface TunerSessionFactory { + + /** Called when a session is released */ + interface SessionReleasedCallback { + + /** + * Called when the given session is released. + * + * @param session The session that has been released. + */ + void onReleased(Session session); + } + + Session create( + Context context, + ChannelDataManager channelDataManager, + SessionReleasedCallback releasedCallback); +} diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java new file mode 100644 index 00000000..54e959e6 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java @@ -0,0 +1,49 @@ +package com.android.tv.tuner.tvinput.factory; + +import android.content.Context; +import android.media.tv.TvInputService.Session; +import com.android.tv.tuner.source.TsDataSourceManager; +import com.android.tv.tuner.tvinput.TunerSession; +import com.android.tv.tuner.tvinput.TunerSessionExoV2; +import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; +import com.android.tv.common.flags.TunerFlags; +import javax.inject.Inject; + +/** Creates a {@link TunerSessionFactory}. */ +public class TunerSessionFactoryImpl implements TunerSessionFactory { + + private final TunerFlags mTunerFlags; + private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + private final TsDataSourceManager.Factory mTsDataSourceManagerFactory; + + @Inject + public TunerSessionFactoryImpl( + TunerFlags tunerFlags, + ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags, + TsDataSourceManager.Factory tsDataSourceManagerFactory) { + mTunerFlags = tunerFlags; + mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags; + mTsDataSourceManagerFactory = tsDataSourceManagerFactory; + } + + @Override + public Session create( + Context context, + ChannelDataManager channelDataManager, + SessionReleasedCallback releasedCallback) { + return mTunerFlags.useExoplayerV2() + ? new TunerSessionExoV2( + context, + channelDataManager, + releasedCallback, + mConcurrentDvrPlaybackFlags, + mTsDataSourceManagerFactory) + : new TunerSession( + context, + channelDataManager, + releasedCallback, + mConcurrentDvrPlaybackFlags, + mTsDataSourceManagerFactory); + } +} diff --git a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java deleted file mode 100644 index fad71335..00000000 --- a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2016 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.tuner.util; - -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; -import android.os.AsyncTask; -import android.os.Build; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.Pair; -import com.android.tv.common.BaseApplication; -import com.android.tv.common.BuildConfig; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; - -/** Utility class for providing tuner input info. */ -public class TunerInputInfoUtils { - private static final String TAG = "TunerInputInfoUtils"; - private static final boolean DEBUG = false; - - /** Builds tuner input's info. */ - @Nullable - @TargetApi(Build.VERSION_CODES.N) - public static TvInputInfo buildTunerInputInfo(Context context) { - Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context); - if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) { - return null; - } - int inputLabelId = 0; - switch (tunerTypeAndCount.first) { - case TunerHal.TUNER_TYPE_BUILT_IN: - inputLabelId = R.string.bt_app_name; - break; - case TunerHal.TUNER_TYPE_USB: - inputLabelId = R.string.ut_app_name; - break; - case TunerHal.TUNER_TYPE_NETWORK: - inputLabelId = R.string.nt_app_name; - break; - } - try { - String inputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); - TvInputInfo.Builder builder = - new TvInputInfo.Builder(context, ComponentName.unflattenFromString(inputId)); - return builder.setLabel(inputLabelId) - .setCanRecord(CommonFeatures.DVR.isEnabled(context)) - .setTunerCount(tunerTypeAndCount.second) - .build(); - } catch (IllegalArgumentException | NullPointerException e) { - // BaseTunerTvInputService is not enabled. - return null; - } - } - - /** - * Updates tuner input's info. - * - * @param context {@link Context} instance - */ - public static void updateTunerInputInfo(Context context) { - final Context appContext = context.getApplicationContext(); - if (!BuildConfig.NO_JNI_TEST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - new AsyncTask<Void, Void, TvInputInfo>() { - @Override - protected TvInputInfo doInBackground(Void... params) { - if (DEBUG) Log.d(TAG, "updateTunerInputInfo()"); - return buildTunerInputInfo(appContext); - } - - @Override - @TargetApi(Build.VERSION_CODES.N) - protected void onPostExecute(TvInputInfo info) { - if (info != null) { - ((TvInputManager) appContext.getSystemService(Context.TV_INPUT_SERVICE)) - .updateTvInputInfo(info); - if (DEBUG) { - Log.d( - TAG, - "TvInputInfo [" - + info.loadLabel(appContext) - + "] updated: " - + info.toString()); - } - } else { - if (DEBUG) { - Log.d(TAG, "Updating tuner input info failed. Input is not ready yet."); - } - } - } - }.execute(); - } - } -} diff --git a/tuner/tests/testing/Android.mk b/tuner/tests/testing/Android.mk index c0d5dda9..79e35e5a 100644 --- a/tuner/tests/testing/Android.mk +++ b/tuner/tests/testing/Android.mk @@ -8,11 +8,10 @@ LOCAL_SRC_FILES := \ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-annotations \ - android-support-test \ - guava \ + androidx.test.runner \ + tv-guava-android-jar \ mockito-target \ - platform-robolectric-3.6.2-prebuilt \ - truth-0-36-prebuilt-jar \ + tv-lib-truth \ ub-uiautomator \ # Link tv-common as shared library to avoid the problem of initialization of the constants diff --git a/tuner/tests/testing/AndroidManifest.xml b/tuner/tests/testing/AndroidManifest.xml index f244ae7b..7e07a52a 100644 --- a/tuner/tests/testing/AndroidManifest.xml +++ b/tuner/tests/testing/AndroidManifest.xml @@ -18,6 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tuner.testing" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application /> </manifest> diff --git a/tuner/tests/unittests/javatests/AndroidManifest.xml b/tuner/tests/unittests/javatests/AndroidManifest.xml index 8a5fda8f..62caefa1 100644 --- a/tuner/tests/unittests/javatests/AndroidManifest.xml +++ b/tuner/tests/unittests/javatests/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tuner.layout.tests" > - <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/> + <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27"/> <instrumentation android:name="android.test.InstrumentationTestRunner" diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml index 9c815600..6fe0b85a 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml @@ -18,10 +18,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tuner.tests" > - <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="26" /> + <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27" /> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" + android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.tv" /> <application android:label="TunerTvInputTests" > diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java index cc4f6fde..6d113b0f 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java @@ -25,19 +25,19 @@ public class FakeTunerHal extends TunerHal { } @Override - protected boolean openFirstAvailable() { + public boolean openFirstAvailable() { mDeviceOpened = true; getDeliverySystemTypeFromDevice(); return true; } @Override - protected boolean isDeviceOpen() { + public boolean isDeviceOpen() { return mDeviceOpened; } @Override - protected long getDeviceId() { + public long getDeviceId() { return 0; } diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java index 73d234e0..cb464839 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java @@ -76,7 +76,7 @@ public class FileTunerHal extends TunerHal { } @Override - protected boolean openFirstAvailable() { + public boolean openFirstAvailable() { sIsDeviceOpen = true; getDeliverySystemTypeFromDevice(); return true; @@ -86,12 +86,12 @@ public class FileTunerHal extends TunerHal { public void close() {} @Override - protected boolean isDeviceOpen() { + public boolean isDeviceOpen() { return sIsDeviceOpen; } @Override - protected long getDeviceId() { + public long getDeviceId() { return DEVICE_ID; } diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java index 0e9bd357..ef653f86 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java @@ -21,10 +21,11 @@ import android.graphics.SurfaceTexture; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import android.support.test.filters.LargeTest; import android.test.InstrumentationTestCase; import android.util.Log; import android.view.Surface; +import androidx.test.filters.LargeTest; +import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.PsiData; import com.android.tv.tuner.data.PsipData; @@ -33,10 +34,10 @@ import com.android.tv.tuner.data.nano.Channel; import com.android.tv.tuner.exoplayer.MpegTsPlayer; import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; import com.android.tv.tuner.exoplayer.buffer.BufferManager; +import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; +import com.android.tv.tuner.ts.EventDetector.EventListener; import com.google.android.exoplayer.ExoPlayer; import java.io.File; import java.io.FileOutputStream; @@ -86,7 +87,9 @@ public class ZappingTimeTest extends InstrumentationTestCase { private AtomicLong mOnDrawnToSurfaceTimeMs = new AtomicLong(0); private MockMpegTsPlayerListener mMpegTsPlayerListener = new MockMpegTsPlayerListener(); private MockPlaybackBufferListener mPlaybackBufferListener = new MockPlaybackBufferListener(); - private MockEventListener mEventListener = new MockEventListener(); + private MockChannelScanListener mEventListener = new MockChannelScanListener(); + private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags = + new DefaultConcurrentDvrPlaybackFlags(); @Override protected void setUp() throws Exception { @@ -152,7 +155,8 @@ public class ZappingTimeTest extends InstrumentationTestCase { new MpegTsRendererBuilder( mTargetContext, bufferManager, - mPlaybackBufferListener), + mPlaybackBufferListener, + mConcurrentDvrPlaybackFlags), mHandler, mSourceManager, null, @@ -388,7 +392,7 @@ public class ZappingTimeTest extends InstrumentationTestCase { } } - private static class MockEventListener implements EventDetector.EventListener { + private static class MockChannelScanListener implements EventListener { @Override public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { if (DEBUG) { diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml index 3e6946a9..77c7f40a 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml @@ -17,7 +17,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tv.tuner" android:versionCode="1"> - <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/> + <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/> <application android:label="TunerTvInputLayoutTests" > <activity android:name="com.android.tv.tuner.layout.tests.ScaledLayoutActivity" android:label="ScaledLayout Test" /> diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java index 214b0631..c2a23f2f 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java @@ -20,11 +20,11 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import android.content.Intent; -import android.support.test.filters.SmallTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; import android.view.View; import android.widget.FrameLayout; +import androidx.test.filters.SmallTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; import com.android.tv.tuner.layout.ScaledLayout; import org.junit.After; import org.junit.Before; diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java index 2354c827..a3a32084 100644 --- a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java +++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java @@ -21,27 +21,27 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import android.os.AsyncTask; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalFactory; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; +import com.android.tv.tuner.api.Tuner; +import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalCreator; import java.util.concurrent.Executor; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for {@link TunerHalFactory}. */ +/** Tests for {@link TunerHalCreator}. */ @SmallTest @RunWith(AndroidJUnit4.class) -public class TunerHalFactoryTest { +public class TunerHalCreatorTest { private final FakeExecutor mFakeExecutor = new FakeExecutor(); - private static class TestTunerHalFactory extends TunerHalFactory { - private TestTunerHalFactory(Executor executor) { + private static class TestTunerHalCreator extends TunerHalCreator { + private TestTunerHalCreator(Executor executor) { super(null, executor); } @Override - protected TunerHal createInstance() { + protected Tuner createInstance() { return new com.android.tv.tuner.FakeTunerHal() {}; } } @@ -61,29 +61,29 @@ public class TunerHalFactoryTest { @Test public void test_asyncGet() { - TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor); - assertNull(tunerHalFactory.mTunerHal); - tunerHalFactory.generate(); - assertNull(tunerHalFactory.mTunerHal); + TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor); + assertNull(tunerHalCreator.mTunerHal); + tunerHalCreator.generate(); + assertNull(tunerHalCreator.mTunerHal); mFakeExecutor.executeActually(); - TunerHal tunerHal = tunerHalFactory.getOrCreate(); + Tuner tunerHal = tunerHalCreator.getOrCreate(); assertNotNull(tunerHal); - assertSame(tunerHal, tunerHalFactory.getOrCreate()); - tunerHalFactory.clear(); + assertSame(tunerHal, tunerHalCreator.getOrCreate()); + tunerHalCreator.clear(); } @Test public void test_syncGet() { - TunerHalFactory tunerHalFactory = new TestTunerHalFactory(AsyncTask.SERIAL_EXECUTOR); - assertNull(tunerHalFactory.mTunerHal); - tunerHalFactory.generate(); - assertNotNull(tunerHalFactory.getOrCreate()); + TunerHalCreator tunerHalCreator = new TestTunerHalCreator(AsyncTask.SERIAL_EXECUTOR); + assertNull(tunerHalCreator.mTunerHal); + tunerHalCreator.generate(); + assertNotNull(tunerHalCreator.getOrCreate()); } @Test public void test_syncGetWithoutGenerate() { - TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor); - assertNull(tunerHalFactory.mTunerHal); - assertNotNull(tunerHalFactory.getOrCreate()); + TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor); + assertNull(tunerHalCreator.mTunerHal); + assertNotNull(tunerHalCreator.getOrCreate()); } } |