aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/src/com')
-rw-r--r--tuner/src/com/android/tv/tuner/DvbTunerHal.java53
-rw-r--r--tuner/src/com/android/tv/tuner/TunerFeatures.java103
-rw-r--r--tuner/src/com/android/tv/tuner/TunerHal.java190
-rw-r--r--tuner/src/com/android/tv/tuner/api/ChannelScanListener.java30
-rw-r--r--tuner/src/com/android/tv/tuner/api/ScanChannel.java55
-rw-r--r--tuner/src/com/android/tv/tuner/api/Tuner.java115
-rw-r--r--tuner/src/com/android/tv/tuner/api/TunerFactory.java31
-rw-r--r--tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java96
-rw-r--r--tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/Cea708Data.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/Cea708Parser.java (renamed from tuner/src/com/android/tv/tuner/cc/Cea708Parser.java)3
-rw-r--r--tuner/src/com/android/tv/tuner/data/PsipData.java1
-rw-r--r--tuner/src/com/android/tv/tuner/data/SectionParser.java (renamed from tuner/src/com/android/tv/tuner/ts/SectionParser.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java61
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java11
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java13
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java29
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java25
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java40
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java8
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java44
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java19
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java (renamed from tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java16
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java101
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java1
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java4
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java143
-rw-r--r--tuner/src/com/android/tv/tuner/features/TunerFeatures.java59
-rw-r--r--tuner/src/com/android/tv/tuner/layout/ScaledLayout.java13
-rw-r--r--tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java14
-rw-r--r--tuner/src/com/android/tv/tuner/modules/TunerModule.java23
-rw-r--r--tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java18
-rw-r--r--tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java (renamed from tuner/src/com/android/tv/tuner/TunerPreferences.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java177
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java (renamed from tuner/src/com/android/tv/tuner/ChannelScanFileParser.java)48
-rw-r--r--tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java48
-rw-r--r--tuner/src/com/android/tv/tuner/setup/LocationFragment.java235
-rw-r--r--tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java19
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ScanFragment.java89
-rw-r--r--tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java10
-rw-r--r--tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java14
-rw-r--r--tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java21
-rw-r--r--tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java (renamed from tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java)7
-rw-r--r--tuner/src/com/android/tv/tuner/source/FileTsStreamer.java9
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsDataSource.java5
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java61
-rw-r--r--tuner/src/com/android/tv/tuner/source/TsStreamer.java6
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerSourceModule.java31
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java41
-rw-r--r--tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java79
-rw-r--r--tuner/src/com/android/tv/tuner/ts/EventDetector.java (renamed from tuner/src/com/android/tv/tuner/tvinput/EventDetector.java)35
-rw-r--r--tuner/src/com/android/tv/tuner/ts/TsParser.java3
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java80
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java55
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java30
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java235
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSession.java200
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java206
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java192
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java1220
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java2073
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java4
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java (renamed from tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java)43
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java (renamed from tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java)2
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java25
-rw-r--r--tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java49
-rw-r--r--tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java112
70 files changed, 5240 insertions, 1555 deletions
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();
- }
- }
-}