diff options
Diffstat (limited to 'tuner/src/com')
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(); - } - } -} |