diff options
Diffstat (limited to 'src')
404 files changed, 21489 insertions, 43070 deletions
diff --git a/src/com/android/exoplayer/MediaFormatUtil.java b/src/com/android/exoplayer/MediaFormatUtil.java deleted file mode 100644 index d7a981f6..00000000 --- a/src/com/android/exoplayer/MediaFormatUtil.java +++ /dev/null @@ -1,76 +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.google.android.exoplayer; - -import android.support.annotation.Nullable; - -import com.google.android.exoplayer.util.MimeTypes; - -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** {@link MediaFormat} creation helper util */ -public class MediaFormatUtil { - - /** - * Creates {@link MediaFormat} from {@link android.media.MediaFormat}. - * Since {@link com.google.android.exoplayer.TrackRenderer} uses {@link MediaFormat}, - * {@link android.media.MediaFormat} should be converted to be used with ExoPlayer. - */ - public static MediaFormat createMediaFormat(android.media.MediaFormat format) { - String mimeType = format.getString(android.media.MediaFormat.KEY_MIME); - String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE); - int maxInputSize = - getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE); - int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH); - int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); - int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees"); - int channelCount = - getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); - int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); - int encoderDelay = getOptionalIntegerV16(format, "encoder-delay"); - int encoderPadding = getOptionalIntegerV16(format, "encoder-padding"); - ArrayList<byte[]> initializationData = new ArrayList<>(); - for (int i = 0; format.containsKey("csd-" + i); i++) { - ByteBuffer buffer = format.getByteBuffer("csd-" + i); - byte[] data = new byte[buffer.limit()]; - buffer.get(data); - initializationData.add(data); - buffer.flip(); - } - long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) - ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; - int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT - : MediaFormat.NO_VALUE; - MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE, - maxInputSize, durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE, - channelCount, sampleRate, language, MediaFormat.OFFSET_SAMPLE_RELATIVE, - initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, pcmEncoding, - encoderDelay, encoderPadding, null, MediaFormat.NO_VALUE); - mediaFormat.setFrameworkFormatV16(format); - return mediaFormat; - } - - @Nullable - private static String getOptionalStringV16(android.media.MediaFormat format, String key) { - return format.containsKey(key) ? format.getString(key) : null; - } - - private static int getOptionalIntegerV16(android.media.MediaFormat format, String key) { - return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE; - } - -} diff --git a/src/com/android/exoplayer/MediaSoftwareCodecUtil.java b/src/com/android/exoplayer/MediaSoftwareCodecUtil.java deleted file mode 100644 index 8c2509d4..00000000 --- a/src/com/android/exoplayer/MediaSoftwareCodecUtil.java +++ /dev/null @@ -1,280 +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.google.android.exoplayer; - -import android.annotation.TargetApi; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.util.MimeTypes; - -import java.util.HashMap; - -/** - * Mostly copied from {@link com.google.android.exoplayer.MediaCodecUtil} in order to choose - * software codec over hardware codec. - */ -public class MediaSoftwareCodecUtil { - private static final String TAG = "MediaSoftwareCodecUtil"; - - /** - * Thrown when an error occurs querying the device for its underlying media capabilities. - * <p> - * Such failures are not expected in normal operation and are normally temporary (e.g. if the - * mediaserver process has crashed and is yet to restart). - */ - public static class DecoderQueryException extends Exception { - - private DecoderQueryException(Throwable cause) { - super("Failed to query underlying media codecs", cause); - } - - } - - private static final HashMap<CodecKey, Pair<String, MediaCodecInfo.CodecCapabilities>> - sSwCodecs = new HashMap<>(); - - /** - * Gets information about the software decoder that will be used for a given mime type. - */ - public static DecoderInfo getSoftwareDecoderInfo(String mimeType, boolean secure) - throws DecoderQueryException { - // TODO: Add a test for this method. - Pair<String, MediaCodecInfo.CodecCapabilities> info = - getMediaSoftwareCodecInfo(mimeType, secure); - if (info == null) { - return null; - } - return new DecoderInfo(info.first, info.second); - } - - /** - * Returns the name of the software decoder and its capabilities for the given mimeType. - */ - private static synchronized Pair<String, MediaCodecInfo.CodecCapabilities> - getMediaSoftwareCodecInfo(String mimeType, boolean secure) throws DecoderQueryException { - CodecKey key = new CodecKey(mimeType, secure); - if (sSwCodecs.containsKey(key)) { - return sSwCodecs.get(key); - } - MediaCodecListCompat mediaCodecList = new MediaCodecListCompatV21(secure); - Pair<String, MediaCodecInfo.CodecCapabilities> codecInfo = - getMediaSoftwareCodecInfo(key, mediaCodecList); - if (secure && codecInfo == null) { - // Some devices don't list secure decoders on API level 21. Try the legacy path. - mediaCodecList = new MediaCodecListCompatV16(); - codecInfo = getMediaSoftwareCodecInfo(key, mediaCodecList); - if (codecInfo != null) { - Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType - + ". Assuming: " + codecInfo.first); - } - } - return codecInfo; - } - - private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfo( - CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { - try { - return getMediaSoftwareCodecInfoInternal(key, mediaCodecList); - } catch (Exception e) { - // If the underlying mediaserver is in a bad state, we may catch an - // IllegalStateException or an IllegalArgumentException here. - throw new DecoderQueryException(e); - } - } - - private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfoInternal( - CodecKey key, MediaCodecListCompat mediaCodecList) { - String mimeType = key.mimeType; - int numberOfCodecs = mediaCodecList.getCodecCount(); - boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); - // Note: MediaCodecList is sorted by the framework such that the best decoders come first. - for (int i = 0; i < numberOfCodecs; i++) { - MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i); - String codecName = info.getName(); - if (!info.isEncoder() && codecName.startsWith("OMX.google.") - && (secureDecodersExplicit || !codecName.endsWith(".secure"))) { - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(mimeType)) { - MediaCodecInfo.CodecCapabilities capabilities = - info.getCapabilitiesForType(supportedType); - boolean secure = mediaCodecList.isSecurePlaybackSupported( - key.mimeType, capabilities); - if (!secureDecodersExplicit) { - // Cache variants for both insecure and (if we think it's supported) - // secure playback. - sSwCodecs.put(key.secure ? new CodecKey(mimeType, false) : key, - Pair.create(codecName, capabilities)); - if (secure) { - sSwCodecs.put(key.secure ? key : new CodecKey(mimeType, true), - Pair.create(codecName + ".secure", capabilities)); - } - } else { - // Only cache this variant. If both insecure and secure decoders are - // available, they should both be listed separately. - sSwCodecs.put( - key.secure == secure ? key: new CodecKey(mimeType, secure), - Pair.create(codecName, capabilities)); - } - if (sSwCodecs.containsKey(key)) { - return sSwCodecs.get(key); - } - } - } - } - } - sSwCodecs.put(key, null); - return null; - } - - private interface MediaCodecListCompat { - - /** - * Returns the number of codecs in the list. - */ - int getCodecCount(); - - /** - * Returns the info at the specified index in the list. - * - * @param index The index. - */ - MediaCodecInfo getCodecInfoAt(int index); - - /** - * Returns whether secure decoders are explicitly listed, if present. - */ - boolean secureDecodersExplicit(); - - /** - * Returns true if secure playback is supported for the given - * {@link android.media.MediaCodecInfo.CodecCapabilities}, which should - * have been obtained from a {@link MediaCodecInfo} obtained from this list. - */ - boolean isSecurePlaybackSupported(String mimeType, - MediaCodecInfo.CodecCapabilities capabilities); - - } - - @TargetApi(21) - private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { - - private final int codecKind; - - private MediaCodecInfo[] mediaCodecInfos; - - public MediaCodecListCompatV21(boolean includeSecure) { - codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS; - } - - @Override - public int getCodecCount() { - ensureMediaCodecInfosInitialized(); - return mediaCodecInfos.length; - } - - @Override - public MediaCodecInfo getCodecInfoAt(int index) { - ensureMediaCodecInfosInitialized(); - return mediaCodecInfos[index]; - } - - @Override - public boolean secureDecodersExplicit() { - return true; - } - - @Override - public boolean isSecurePlaybackSupported(String mimeType, - MediaCodecInfo.CodecCapabilities capabilities) { - return capabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); - } - - private void ensureMediaCodecInfosInitialized() { - if (mediaCodecInfos == null) { - mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); - } - } - - } - - @SuppressWarnings("deprecation") - private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { - - @Override - public int getCodecCount() { - return MediaCodecList.getCodecCount(); - } - - @Override - public MediaCodecInfo getCodecInfoAt(int index) { - return MediaCodecList.getCodecInfoAt(index); - } - - @Override - public boolean secureDecodersExplicit() { - return false; - } - - @Override - public boolean isSecurePlaybackSupported(String mimeType, - MediaCodecInfo.CodecCapabilities capabilities) { - // Secure decoders weren't explicitly listed prior to API level 21. We assume that - // a secure H264 decoder exists. - return MimeTypes.VIDEO_H264.equals(mimeType); - } - - } - - private static final class CodecKey { - - public final String mimeType; - public final boolean secure; - - public CodecKey(String mimeType, boolean secure) { - this.mimeType = mimeType; - this.secure = secure; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); - result = 2 * result + (secure ? 0 : 1); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof CodecKey)) { - return false; - } - CodecKey other = (CodecKey) obj; - return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure; - } - - } - -} diff --git a/src/com/android/exoplayer/text/SubtitleView.java b/src/com/android/exoplayer/text/SubtitleView.java deleted file mode 100644 index 37926eda..00000000 --- a/src/com/android/exoplayer/text/SubtitleView.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.text; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Join; -import android.graphics.Paint.Style; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.text.Layout.Alignment; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.View; - -import com.google.android.exoplayer.util.Util; - -import java.util.ArrayList; - -/** - * Since this class does not exist in recent version of ExoPlayer and used by - * {@link com.android.tv.tuner.cc.CaptionWindowLayout}, this class is copied from - * older version of ExoPlayer. - * A view for rendering a single caption. - */ -@Deprecated -public class SubtitleView extends View { - /** - * Ratio of inner padding to font size. - */ - private static final float INNER_PADDING_RATIO = 0.125f; - - /** - * Temporary rectangle used for computing line bounds. - */ - private final RectF mLineBounds = new RectF(); - - // Styled dimensions. - private final float mCornerRadius; - private final float mOutlineWidth; - private final float mShadowRadius; - private final float mShadowOffset; - - private final TextPaint mTextPaint; - private final Paint mPaint; - - private CharSequence mText; - - private int mForegroundColor; - private int mBackgroundColor; - private int mEdgeColor; - private int mEdgeType; - - private boolean mHasMeasurements; - private int mLastMeasuredWidth; - private StaticLayout mLayout; - - private Alignment mAlignment; - private final float mSpacingMult; - private final float mSpacingAdd; - private int mInnerPaddingX; - private float mWhiteSpaceWidth; - private ArrayList<Integer> mPrefixSpaces = new ArrayList<>(); - - public SubtitleView(Context context) { - this(context, null); - } - - public SubtitleView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - int[] viewAttr = {android.R.attr.text, android.R.attr.textSize, - android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; - TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0); - CharSequence text = a.getText(0); - int textSize = a.getDimensionPixelSize(1, 15); - mSpacingAdd = a.getDimensionPixelSize(2, 0); - mSpacingMult = a.getFloat(3, 1); - a.recycle(); - - Resources resources = getContext().getResources(); - DisplayMetrics displayMetrics = resources.getDisplayMetrics(); - int twoDpInPx = - Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); - mCornerRadius = twoDpInPx; - mOutlineWidth = twoDpInPx; - mShadowRadius = twoDpInPx; - mShadowOffset = twoDpInPx; - - mTextPaint = new TextPaint(); - mTextPaint.setAntiAlias(true); - mTextPaint.setSubpixelText(true); - - mAlignment = Alignment.ALIGN_CENTER; - - mPaint = new Paint(); - mPaint.setAntiAlias(true); - - mInnerPaddingX = 0; - setText(text); - setTextSize(textSize); - setStyle(CaptionStyleCompat.DEFAULT); - } - - @Override - public void setBackgroundColor(int color) { - mBackgroundColor = color; - forceUpdate(false); - } - - /** - * Sets the text to be displayed by the view. - * - * @param text The text to display. - */ - public void setText(CharSequence text) { - this.mText = text; - forceUpdate(true); - } - - /** - * Sets the text size in pixels. - * - * @param size The text size in pixels. - */ - public void setTextSize(float size) { - if (mTextPaint.getTextSize() != size) { - mTextPaint.setTextSize(size); - mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f); - mWhiteSpaceWidth -= mInnerPaddingX * 2; - forceUpdate(true); - } - } - - /** - * Sets the text alignment. - * - * @param textAlignment The text alignment. - */ - public void setTextAlignment(Alignment textAlignment) { - mAlignment = textAlignment; - } - - /** - * Configures the view according to the given style. - * - * @param style A style for the view. - */ - public void setStyle(CaptionStyleCompat style) { - mForegroundColor = style.foregroundColor; - mBackgroundColor = style.backgroundColor; - mEdgeType = style.edgeType; - mEdgeColor = style.edgeColor; - setTypeface(style.typeface); - super.setBackgroundColor(style.windowColor); - forceUpdate(true); - } - - public void setPrefixSpaces(ArrayList<Integer> prefixSpaces) { - mPrefixSpaces = prefixSpaces; - } - - public void setWhiteSpaceWidth(float whiteSpaceWidth) { - mWhiteSpaceWidth = whiteSpaceWidth; - } - - private void setTypeface(Typeface typeface) { - if (mTextPaint.getTypeface() != typeface) { - mTextPaint.setTypeface(typeface); - forceUpdate(true); - } - } - - private void forceUpdate(boolean needsLayout) { - if (needsLayout) { - mHasMeasurements = false; - requestLayout(); - } - invalidate(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int widthSpec = MeasureSpec.getSize(widthMeasureSpec); - - if (computeMeasurements(widthSpec)) { - final StaticLayout layout = this.mLayout; - final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2; - final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom(); - int width = 0; - int lineCount = layout.getLineCount(); - for (int i = 0; i < lineCount; i++) { - width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width); - } - width += paddingX; - setMeasuredDimension(width, height); - } else if (Util.SDK_INT >= 11) { - setTooSmallMeasureDimensionV11(); - } else { - setMeasuredDimension(0, 0); - } - } - - @TargetApi(11) - private void setTooSmallMeasureDimensionV11() { - setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL); - } - - @Override - public void onLayout(boolean changed, int l, int t, int r, int b) { - final int width = r - l; - computeMeasurements(width); - } - - private boolean computeMeasurements(int maxWidth) { - if (mHasMeasurements && maxWidth == mLastMeasuredWidth) { - return true; - } - - // Account for padding. - final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2; - maxWidth -= paddingX; - if (maxWidth <= 0) { - return false; - } - - mHasMeasurements = true; - mLastMeasuredWidth = maxWidth; - mLayout = new StaticLayout(mText, mTextPaint, maxWidth, mAlignment, - mSpacingMult, mSpacingAdd, true); - return true; - } - - @Override - protected void onDraw(Canvas c) { - final StaticLayout layout = this.mLayout; - if (layout == null) { - return; - } - - final int saveCount = c.save(); - final int innerPaddingX = this.mInnerPaddingX; - c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop()); - - final int lineCount = layout.getLineCount(); - final Paint textPaint = this.mTextPaint; - final Paint paint = this.mPaint; - final RectF bounds = mLineBounds; - - if (Color.alpha(mBackgroundColor) > 0) { - final float cornerRadius = this.mCornerRadius; - float previousBottom = layout.getLineTop(0); - - paint.setColor(mBackgroundColor); - paint.setStyle(Style.FILL); - - for (int i = 0; i < lineCount; i++) { - float spacesPadding = 0.0f; - if (i < mPrefixSpaces.size()) { - spacesPadding += mPrefixSpaces.get(i) * mWhiteSpaceWidth; - } - bounds.left = layout.getLineLeft(i) - innerPaddingX + spacesPadding; - bounds.right = layout.getLineRight(i) + innerPaddingX; - bounds.top = previousBottom; - bounds.bottom = layout.getLineBottom(i); - previousBottom = bounds.bottom; - - c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint); - } - } - - if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { - textPaint.setStrokeJoin(Join.ROUND); - textPaint.setStrokeWidth(mOutlineWidth); - textPaint.setColor(mEdgeColor); - textPaint.setStyle(Style.FILL_AND_STROKE); - layout.draw(c); - } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { - textPaint.setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor); - } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED - || mEdgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { - boolean raised = mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; - int colorUp = raised ? Color.WHITE : mEdgeColor; - int colorDown = raised ? mEdgeColor : Color.WHITE; - float offset = mShadowRadius / 2f; - textPaint.setColor(mForegroundColor); - textPaint.setStyle(Style.FILL); - textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp); - layout.draw(c); - textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown); - } - - textPaint.setColor(mForegroundColor); - textPaint.setStyle(Style.FILL); - layout.draw(c); - textPaint.setShadowLayer(0, 0, 0, 0); - c.restoreToCount(saveCount); - } - -} diff --git a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java deleted file mode 100644 index 2b7817dc..00000000 --- a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java +++ /dev/null @@ -1,127 +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.google.android.exoplayer2.ext.ffmpeg; - -import android.content.Context; -import android.content.pm.PackageManager; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; -import com.google.android.exoplayer2.util.MimeTypes; -import com.android.tv.common.SoftPreconditions; - -import java.nio.ByteBuffer; - -/** - * Audio decoder which uses ffmpeg extension of ExoPlayer2. Since {@link FfmpegDecoder} is package - * private, expose the decoder via this class. Supported formats are AC3 and MP2. - */ -public class FfmpegAudioDecoder { - private static final int NUM_DECODER_BUFFERS = 1; - - // The largest AC3 sample size. This is bigger than the largest MP2 sample size (1729). - private static final int INITIAL_INPUT_BUFFER_SIZE = 2560; - private static boolean AVAILABLE; - - static { - AVAILABLE = - FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_AC3) - && FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_MPEG_L2); - } - - private FfmpegDecoder mDecoder; - private DecoderInputBuffer mInputBuffer; - private SimpleOutputBuffer mOutputBuffer; - private boolean mStarted; - - /** Return whether Ffmpeg based software audio decoder is available. */ - public static boolean isAvailable() { - return AVAILABLE; - } - - /** Creates an Ffmpeg based software audio decoder. */ - public FfmpegAudioDecoder(Context context) { - if (context.checkSelfPermission("android.permission.INTERNET") - == PackageManager.PERMISSION_GRANTED) { - throw new IllegalStateException("This code should run in an isolated process"); - } - } - - /** - * Decodes an audio sample. - * - * @param timeUs presentation timestamp of the sample - * @param sample data - */ - public void decode(long timeUs, byte[] sample) { - SoftPreconditions.checkState(AVAILABLE); - mInputBuffer.data.clear(); - mInputBuffer.data.put(sample); - mInputBuffer.data.flip(); - mInputBuffer.timeUs = timeUs; - mDecoder.decode(mInputBuffer, mOutputBuffer, !mStarted); - if (!mStarted) { - mStarted = true; - } - } - - /** Returns a decoded sample from decoder. */ - public ByteBuffer getDecodedSample() { - return mOutputBuffer.data; - } - - /** Returns the presentation time for the decoded sample. */ - public long getDecodedTimeUs() { - return mOutputBuffer.timeUs; - } - - /** - * Clear previous decode state if any. Prepares to decode samples of the specified encoding. - * This method should be called before using decode. - * - * @param mime audio encoding - */ - public void resetDecoderState(String mime) { - SoftPreconditions.checkState(AVAILABLE); - release(); - try { - mDecoder = - new FfmpegDecoder( - NUM_DECODER_BUFFERS, - NUM_DECODER_BUFFERS, - INITIAL_INPUT_BUFFER_SIZE, - mime, - null); - mStarted = false; - mInputBuffer = mDecoder.createInputBuffer(); - // Since native JNI requires direct buffer, we should allocate it by #allocateDirect. - mInputBuffer.data = ByteBuffer.allocateDirect(INITIAL_INPUT_BUFFER_SIZE); - mOutputBuffer = mDecoder.createOutputBuffer(); - } catch (FfmpegDecoderException e) { - // if AVAILABLE is {@code true}, this will not happen. - } - } - - /** Releases all the resource. */ - public void release() { - SoftPreconditions.checkState(AVAILABLE); - if (mDecoder != null) { - mDecoder.release(); - mInputBuffer = null; - mOutputBuffer = null; - mDecoder = null; - } - } -} diff --git a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java deleted file mode 100644 index daa77340..00000000 --- a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ /dev/null @@ -1,84 +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.google.android.exoplayer2.ext.ffmpeg; - -import com.google.android.exoplayer2.util.LibraryLoader; -import com.google.android.exoplayer2.util.MimeTypes; - -/** - * This class is based on com.google.android.exoplayer2.ext.ffmpeg.FfmpegLibrary from ExoPlayer2 - * in order to support mp2 decoder. - * Configures and queries the underlying native library. - */ -public final class FfmpegLibrary { - - private static final LibraryLoader LOADER = - new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); - - private FfmpegLibrary() {} - - /** - * Overrides the names of the FFmpeg native libraries. If an application wishes to call this - * method, it must do so before calling any other method defined by this class, and before - * instantiating a {@link FfmpegAudioRenderer} instance. - */ - public static void setLibraries(String... libraries) { - LOADER.setLibraries(libraries); - } - - /** - * Returns whether the underlying library is available, loading it if necessary. - */ - public static boolean isAvailable() { - return LOADER.isAvailable(); - } - - /** - * Returns the version of the underlying library if available, or null otherwise. - */ - public static String getVersion() { - return isAvailable() ? ffmpegGetVersion() : null; - } - - /** - * Returns whether the underlying library supports the specified MIME type. - */ - public static boolean supportsFormat(String mimeType) { - if (!isAvailable()) { - return false; - } - String codecName = getCodecName(mimeType); - return codecName != null && ffmpegHasDecoder(codecName); - } - - /** - * Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}. - */ - /* package */ static String getCodecName(String mimeType) { - switch (mimeType) { - case MimeTypes.AUDIO_MPEG_L2: - return "mp2"; - case MimeTypes.AUDIO_AC3: - return "ac3"; - default: - return null; - } - } - - private static native String ffmpegGetVersion(); - private static native boolean ffmpegHasDecoder(String codecName); - -} diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/AudioManagerHelper.java index 4fca06ac..942d431d 100644 --- a/src/com/android/tv/AudioManagerHelper.java +++ b/src/com/android/tv/AudioManagerHelper.java @@ -1,66 +1,94 @@ +/* + * 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; import android.app.Activity; import android.content.Context; import android.media.AudioManager; import android.os.Build; - import com.android.tv.receiver.AudioCapabilitiesReceiver; import com.android.tv.ui.TunableTvView; +import com.android.tv.ui.TunableTvViewPlayingApi; -/** - * A helper class to help {@link MainActivity} to handle audio-related stuffs. - */ +/** A helper class to help {@link MainActivity} to handle audio-related stuffs. */ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { private static final float AUDIO_MAX_VOLUME = 1.0f; private static final float AUDIO_MIN_VOLUME = 0.0f; private static final float AUDIO_DUCKING_VOLUME = 0.3f; private final Activity mActivity; - private final TunableTvView mTvView; + private final TunableTvViewPlayingApi mTvView; private final AudioManager mAudioManager; private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; private boolean mAc3PassthroughSupported; private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; - AudioManagerHelper(Activity activity, TunableTvView tvView) { + AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) { mActivity = activity; mTvView = tvView; mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE); - mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(activity, - new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() { - @Override - public void onAc3PassthroughCapabilityChange(boolean capability) { - mAc3PassthroughSupported = capability; - } - }); + mAudioCapabilitiesReceiver = + new AudioCapabilitiesReceiver( + activity, + new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() { + @Override + public void onAc3PassthroughCapabilityChange(boolean capability) { + mAc3PassthroughSupported = capability; + } + }); mAudioCapabilitiesReceiver.register(); } /** - * Sets suitable volume to {@link TunableTvView} according to the current audio focus. - * If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP - * mode, this method will finish the activity. + * Sets suitable volume to {@link TunableTvView} according to the current audio focus. If the + * focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP mode, this + * method will finish the activity. */ void setVolumeByAudioFocusStatus() { if (mTvView.isPlaying()) { switch (mAudioFocusStatus) { case AudioManager.AUDIOFOCUS_GAIN: - mTvView.setStreamVolume(AUDIO_MAX_VOLUME); + if (mTvView.isTimeShiftAvailable()) { + mTvView.timeshiftPlay(); + } else { + mTvView.setStreamVolume(AUDIO_MAX_VOLUME); + } break; case AudioManager.AUDIOFOCUS_LOSS: - if (Features.PICTURE_IN_PICTURE.isEnabled(mActivity) + if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && mActivity.isInPictureInPictureMode()) { mActivity.finish(); break; } + // fall through case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - mTvView.setStreamVolume(AUDIO_MIN_VOLUME); + if (mTvView.isTimeShiftAvailable()) { + mTvView.timeshiftPause(); + } else { + mTvView.setStreamVolume(AUDIO_MIN_VOLUME); + } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); + if (mTvView.isTimeShiftAvailable()) { + mTvView.timeshiftPause(); + } else { + mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); + } break; } } @@ -71,31 +99,28 @@ class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener { * returned result. */ void requestAudioFocus() { - int result = mAudioManager.requestAudioFocus(this, - AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); - mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? - AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS; + int result = + mAudioManager.requestAudioFocus( + this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + mAudioFocusStatus = + (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) + ? AudioManager.AUDIOFOCUS_GAIN + : AudioManager.AUDIOFOCUS_LOSS; setVolumeByAudioFocusStatus(); } - /** - * Abandons audio focus. - */ + /** Abandons audio focus. */ void abandonAudioFocus() { mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; mAudioManager.abandonAudioFocus(this); } - /** - * Returns {@code true} if the device supports AC3 pass-through. - */ + /** Returns {@code true} if the device supports AC3 pass-through. */ boolean isAc3PassthroughSupported() { return mAc3PassthroughSupported; } - /** - * Release the resources the helper class may occupied. - */ + /** Release the resources the helper class may occupied. */ void release() { mAudioCapabilitiesReceiver.unregister(); } diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index faa27bbd..8ab145a4 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -24,12 +24,10 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.util.ArraySet; import android.util.Log; - import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvInputManagerHelper; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -57,11 +55,9 @@ public class ChannelTuner { private final Handler mHandler = new Handler(); private final ChannelDataManager mChannelDataManager; private final Set<Listener> mListeners = new ArraySet<>(); - @Nullable - private Channel mCurrentChannel; + @Nullable private Channel mCurrentChannel; private final TvInputManagerHelper mInputManager; - @Nullable - private TvInputInfo mCurrentChannelInputInfo; + @Nullable private TvInputInfo mCurrentChannelInputInfo; private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @@ -86,16 +82,14 @@ public class ChannelTuner { l.onBrowsableChannelListChanged(); } } - }; + }; public ChannelTuner(ChannelDataManager channelDataManager, TvInputManagerHelper inputManager) { mChannelDataManager = channelDataManager; mInputManager = inputManager; } - /** - * Starts ChannelTuner. It cannot be called twice before calling {@link #stop}. - */ + /** Starts ChannelTuner. It cannot be called twice before calling {@link #stop}. */ public void start() { if (mStarted) { throw new IllegalStateException("start is called twice"); @@ -103,18 +97,17 @@ public class ChannelTuner { mStarted = true; mChannelDataManager.addListener(mChannelDataManagerListener); if (mChannelDataManager.isDbLoadFinished()) { - mHandler.post(new Runnable() { - @Override - public void run() { - mChannelDataManagerListener.onLoadFinished(); - } - }); + mHandler.post( + new Runnable() { + @Override + public void run() { + mChannelDataManagerListener.onLoadFinished(); + } + }); } } - /** - * Stops ChannelTuner. - */ + /** Stops ChannelTuner. */ public void stop() { if (!mStarted) { return; @@ -130,30 +123,22 @@ public class ChannelTuner { mChannelDataManagerLoaded = false; } - /** - * Returns true, if all the channels are loaded. - */ + /** Returns true, if all the channels are loaded. */ public boolean areAllChannelsLoaded() { return mChannelDataManagerLoaded; } - /** - * Returns browsable channel lists. - */ + /** Returns browsable channel lists. */ public List<Channel> getBrowsableChannelList() { return Collections.unmodifiableList(mBrowsableChannels); } - /** - * Returns the number of browsable channels. - */ + /** Returns the number of browsable channels. */ public int getBrowsableChannelCount() { return mBrowsableChannels.size(); } - /** - * Returns the current channel. - */ + /** Returns the current channel. */ @Nullable public Channel getCurrentChannel() { return mCurrentChannel; @@ -169,16 +154,12 @@ public class ChannelTuner { mCurrentChannel = currentChannel; } - /** - * Returns the current channel's ID. - */ + /** Returns the current channel's ID. */ public long getCurrentChannelId() { return mCurrentChannel != null ? mCurrentChannel.getId() : Channel.INVALID_ID; } - /** - * Returns the current channel's URI - */ + /** Returns the current channel's URI */ public Uri getCurrentChannelUri() { if (mCurrentChannel == null) { return null; @@ -190,17 +171,13 @@ public class ChannelTuner { } } - /** - * Returns the current {@link TvInputInfo}. - */ + /** Returns the current {@link TvInputInfo}. */ @Nullable public TvInputInfo getCurrentInputInfo() { return mCurrentChannelInputInfo; } - /** - * Returns true, if the current channel is for a passthrough TV input. - */ + /** Returns true, if the current channel is for a passthrough TV input. */ public boolean isCurrentChannelPassthrough() { return mCurrentChannel != null && mCurrentChannel.isPassthrough(); } @@ -208,8 +185,8 @@ public class ChannelTuner { /** * Moves the current channel to the next (or previous) browsable channel. * - * @return true, if the channel is changed to the adjacent channel. If there is no - * browsable channel, it returns false. + * @return true, if the channel is changed to the adjacent channel. If there is no browsable + * channel, it returns false. */ public boolean moveToAdjacentBrowsableChannel(boolean up) { Channel channel = getAdjacentBrowsableChannel(up); @@ -221,8 +198,8 @@ public class ChannelTuner { } /** - * Returns a next browsable channel. It doesn't change the current channel unlike - * {@link #moveToAdjacentBrowsableChannel}. + * Returns a next browsable channel. It doesn't change the current channel unlike {@link + * #moveToAdjacentBrowsableChannel}. */ public Channel getAdjacentBrowsableChannel(boolean up) { if (isCurrentChannelPassthrough() || getBrowsableChannelCount() == 0) { @@ -240,8 +217,7 @@ public class ChannelTuner { } int size = mChannels.size(); for (int i = 0; i < size; ++i) { - int nextChannelIndex = up ? channelIndex + 1 + i - : channelIndex - 1 - i + size; + int nextChannelIndex = up ? channelIndex + 1 + i : channelIndex - 1 - i + size; if (nextChannelIndex >= size) { nextChannelIndex -= size; } @@ -289,7 +265,7 @@ public class ChannelTuner { * as a browsable channel. * * @return true, the channel change is success. But, if the channel doesn't exist, the channel - * change will be failed and it will return false. + * change will be failed and it will return false. */ public boolean moveToChannel(Channel channel) { if (channel == null) { @@ -308,43 +284,29 @@ public class ChannelTuner { return false; } - /** - * Resets the current channel to {@code null}. - */ + /** Resets the current channel to {@code null}. */ public void resetCurrentChannel() { setCurrentChannelAndNotify(null); } - /** - * Adds {@link Listener}. - */ + /** Adds {@link Listener}. */ public void addListener(Listener listener) { mListeners.add(listener); } - /** - * Removes {@link Listener}. - */ + /** Removes {@link Listener}. */ public void removeListener(Listener listener) { mListeners.remove(listener); } public interface Listener { - /** - * Called when all the channels are loaded. - */ + /** Called when all the channels are loaded. */ void onLoadFinished(); - /** - * Called when the browsable channel list is changed. - */ + /** Called when the browsable channel list is changed. */ void onBrowsableChannelListChanged(); - /** - * Called when the current channel is removed. - */ + /** Called when the current channel is removed. */ void onCurrentChannelUnavailable(Channel channel); - /** - * Called when the current channel is changed. - */ + /** Called when the current channel is changed. */ void onChannelChanged(Channel previousChannel, Channel currentChannel); } diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java deleted file mode 100644 index 2052f2e7..00000000 --- a/src/com/android/tv/Features.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.common.feature.Feature; -import com.android.tv.common.feature.GServiceFeature; -import com.android.tv.common.feature.PropertyFeature; -import com.android.tv.config.RemoteConfig; -import com.android.tv.experiments.Experiments; -import com.android.tv.util.LocationUtils; -import com.android.tv.util.PermissionUtils; -import com.android.tv.util.Utils; - -import java.util.Locale; - -import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE; -import static com.android.tv.common.feature.FeatureUtils.AND; -import static com.android.tv.common.feature.FeatureUtils.OFF; -import static com.android.tv.common.feature.FeatureUtils.ON; -import static com.android.tv.common.feature.FeatureUtils.OR; - -/** - * List of {@link Feature} for the Live TV App. - * - * <p>Remove the {@code Feature} once it is launched. - */ -public final class Features { - private static final String TAG = "Features"; - private static final boolean DEBUG = false; - - /** - * UI for opting in to analytics. - * - * <p>Do not turn this on until the splash screen asking existing users to opt-in is launched. - * See <a href="http://b/20228119">b/20228119</a> - */ - public static final Feature ANALYTICS_OPT_IN = ENG_ONLY_FEATURE; - - /** - * Analytics that include sensitive information such as channel or program identifiers. - * - * <p>See <a href="http://b/22062676">b/22062676</a> - */ - public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN); - - public static final Feature EPG_SEARCH = - new PropertyFeature("feature_tv_use_epg_search", false); - - public static final Feature TUNER = - new Feature() { - @Override - public boolean isEnabled(Context context) { - - if (Utils.isDeveloper()) { - // we enable tuner for developers to test tuner in any platform. - return true; - } - - // This is special handling just for USB Tuner. - // It does not require any N API's but relies on a improvements in N for AC3 support - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; - } - }; - - /** - * 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 (Utils.isDeveloper()) { - // Network tuner will be enabled for developers. - return true; - } - return Locale.US.getCountry().equalsIgnoreCase( - LocationUtils.getCurrentCountry(context)); - } - }; - - private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide"; - /** - * A flag which indicates that LC app is unhidden even when there is no input. - */ - public static final Feature UNHIDE = - OR(new GServiceFeature(GSERVICE_KEY_UNHIDE, false), new Feature() { - @Override - public boolean isEnabled(Context context) { - // If LC app runs as non-system app, we unhide the app. - return !PermissionUtils.hasAccessAllEpg(context); - } - }); - - public static final Feature PICTURE_IN_PICTURE = - new Feature() { - private Boolean mEnabled; - - @Override - public boolean isEnabled(Context context) { - if (mEnabled == null) { - mEnabled = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && context.getPackageManager() - .hasSystemFeature( - PackageManager.FEATURE_PICTURE_IN_PICTURE); - } - return mEnabled; - } - }; - - /** 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; - } - }; - - /** Show postal code fragment before channel scan. */ - public static final Feature ENABLE_CLOUD_EPG_REGION = - new Feature() { - private final String[] SUPPORTED_REGIONS = { - }; - - - @Override - public boolean isEnabled(Context context) { - if (!Experiments.CLOUD_EPG.get()) { - if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false"); - return false; - } - String country = LocationUtils.getCurrentCountry(context); - for (int i = 0; i < SUPPORTED_REGIONS.length; i++) { - if (SUPPORTED_REGIONS[i].equalsIgnoreCase(country)) { - return true; - } - } - if (DEBUG) Log.d(TAG, "EPG flag false after country check"); - return false; - } - }; - - /** Enable a conflict dialog between currently watched channel and upcoming recording. */ - public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF; - - /** - * Use input blacklist to disable partner's tuner input. - */ - public static final Feature USE_PARTNER_INPUT_BLACKLIST = ON; - - /** - * Enable Dvb parsers and listeners. - */ - public static final Feature ENABLE_FILE_DVB = OFF; - - @VisibleForTesting - public static final Feature TEST_FEATURE = new PropertyFeature("test_feature", false); - - private Features() { - } -} diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index 2978f409..4f298ed6 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -36,28 +36,27 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; - -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; import com.android.tv.util.TvInputManagerHelper; - import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; /** - * Manages input sessions. - * Responsible for: + * Manages input sessions. Responsible for: + * * <ul> - * <li>Manage {@link TvView} sessions and recording sessions</li> - * <li>Manage capabilities (conflict)</li> + * <li>Manage {@link TvView} sessions and recording sessions + * <li>Manage capabilities (conflict) * </ul> - * <p> - * As TvView's methods should be called on the main thread and the {@link RecordingSession} should - * look at the state of the {@link TvViewSession} when it calls the framework methods, the framework - * calls in RecordingSession are made on the main thread not to introduce the multi-thread problems. + * + * <p>As TvView's methods should be called on the main thread and the {@link RecordingSession} + * should look at the state of the {@link TvViewSession} when it calls the framework methods, the + * framework calls in RecordingSession are made on the main thread not to introduce the multi-thread + * problems. */ @TargetApi(Build.VERSION_CODES.N) public class InputSessionManager { @@ -77,27 +76,25 @@ public class InputSessionManager { public InputSessionManager(Context context) { mContext = context.getApplicationContext(); - mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); + mInputManager = TvSingletons.getSingletons(context).getTvInputManagerHelper(); } /** * Creates the session for {@link TvView}. - * <p> - * Do not call {@link TvView#setCallback} after the session is created. + * + * <p>Do not call {@link TvView#setCallback} after the session is created. */ @MainThread @NonNull - public TvViewSession createTvViewSession(TvView tvView, TunableTvView tunableTvView, - TvInputCallback callback) { + public TvViewSession createTvViewSession( + TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) { TvViewSession session = new TvViewSession(tvView, tunableTvView, callback); mTvViewSessions.add(session); if (DEBUG) Log.d(TAG, "TvView session created: " + session); return session; } - /** - * Releases the {@link TvView} session. - */ + /** Releases the {@link TvView} session. */ @MainThread public void releaseTvViewSession(TvViewSession session) { mTvViewSessions.remove(session); @@ -105,12 +102,14 @@ public class InputSessionManager { if (DEBUG) Log.d(TAG, "TvView session released: " + session); } - /** - * Creates the session for recording. - */ + /** Creates the session for recording. */ @NonNull - public RecordingSession createRecordingSession(String inputId, String tag, - RecordingCallback callback, Handler handler, long endTimeMs) { + public RecordingSession createRecordingSession( + String inputId, + String tag, + RecordingCallback callback, + Handler handler, + long endTimeMs) { RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); mRecordingSessions.add(session); if (DEBUG) Log.d(TAG, "Recording session created: " + session); @@ -120,9 +119,7 @@ public class InputSessionManager { return session; } - /** - * Releases the recording session. - */ + /** Releases the recording session. */ public void releaseRecordingSession(RecordingSession session) { mRecordingSessions.remove(session); session.release(); @@ -132,17 +129,13 @@ public class InputSessionManager { } } - /** - * Adds the {@link OnTvViewChannelChangeListener}. - */ + /** Adds the {@link OnTvViewChannelChangeListener}. */ @MainThread public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { mOnTvViewChannelChangeListeners.add(listener); } - /** - * Removes the {@link OnTvViewChannelChangeListener}. - */ + /** Removes the {@link OnTvViewChannelChangeListener}. */ @MainThread public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { mOnTvViewChannelChangeListeners.remove(listener); @@ -176,9 +169,7 @@ public class InputSessionManager { return null; } - /** - * Retruns the earliest end time of recording sessions in progress of the certain TV input. - */ + /** Retruns the earliest end time of recording sessions in progress of the certain TV input. */ @MainThread public Long getEarliestRecordingSessionEndTimeMs(String inputId) { long timeMs = Long.MAX_VALUE; @@ -240,8 +231,8 @@ public class InputSessionManager { /** * The session for {@link TvView}. - * <p> - * The methods which create or release session for the TV input should be called through this + * + * <p>The methods which create or release session for the TV input should be called through this * session. */ @MainThread @@ -261,31 +252,32 @@ public class InputSessionManager { mTvView = tvView; mTunableTvView = tunableTvView; mCallback = callback; - mTvView.setCallback(new DelegateTvInputCallback(mCallback) { - @Override - public void onConnectionFailed(String inputId) { - if (DEBUG) Log.d(TAG, "TvViewSession: connection failed"); - mTuned = false; - mNeedToBeRetuned = false; - super.onConnectionFailed(inputId); - notifyTvViewChannelChange(null); - } + mTvView.setCallback( + new DelegateTvInputCallback(mCallback) { + @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "TvViewSession: connection failed"); + mTuned = false; + mNeedToBeRetuned = false; + super.onConnectionFailed(inputId); + notifyTvViewChannelChange(null); + } - @Override - public void onDisconnected(String inputId) { - if (DEBUG) Log.d(TAG, "TvViewSession: disconnected"); - mTuned = false; - mNeedToBeRetuned = false; - super.onDisconnected(inputId); - notifyTvViewChannelChange(null); - } - }); + @Override + public void onDisconnected(String inputId) { + if (DEBUG) Log.d(TAG, "TvViewSession: disconnected"); + mTuned = false; + mNeedToBeRetuned = false; + super.onDisconnected(inputId); + notifyTvViewChannelChange(null); + } + }); } /** * Tunes to the channel. - * <p> - * As this is called only for the warming up, there's no need to be retuned. + * + * <p>As this is called only for the warming up, there's no need to be retuned. */ public void tune(String inputId, Uri channelUri) { if (DEBUG) { @@ -299,13 +291,22 @@ public class InputSessionManager { notifyTvViewChannelChange(channelUri); } - /** - * Tunes to the channel. - */ + /** Tunes to the channel. */ public void tune(Channel channel, Bundle params, OnTuneListener listener) { if (DEBUG) { - Log.d(TAG, "tune: {session=" + this + ", channel=" + channel + ", params=" + params - + ", listener=" + listener + ", mTuned=" + mTuned + "}"); + Log.d( + TAG, + "tune: {session=" + + this + + ", channel=" + + channel + + ", params=" + + params + + ", listener=" + + listener + + ", mTuned=" + + mTuned + + "}"); } mChannel = channel; mInputId = channel.getInputId(); @@ -313,8 +314,10 @@ public class InputSessionManager { mParams = params; mOnTuneListener = listener; TvInputInfo input = mInputManager.getTvInputInfo(mInputId); - if (input == null || (input.canRecord() && !isTunedForRecording(mChannelUri) - && getTunedRecordingSessionCount(mInputId) >= input.getTunerCount())) { + if (input == null + || (input.canRecord() + && !isTunedForRecording(mChannelUri) + && getTunedRecordingSessionCount(mInputId) >= input.getTunerCount())) { if (DEBUG) { if (input == null) { Log.d(TAG, "Can't find input for input ID: " + mInputId); @@ -354,9 +357,7 @@ public class InputSessionManager { notifyTvViewChannelChange(null); } - /** - * Resets this TvView. - */ + /** Resets this TvView. */ public void reset() { if (DEBUG) Log.d(TAG, "Reset TvView session"); mTuned = false; @@ -366,8 +367,8 @@ public class InputSessionManager { } void resetByRecording() { - mCallback.onVideoUnavailable(mInputId, - TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE); + mCallback.onVideoUnavailable( + mInputId, TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE); if (mTuned) { if (DEBUG) Log.d(TAG, "Reset TvView session by recording"); mTunableTvView.resetByRecording(); @@ -379,8 +380,8 @@ public class InputSessionManager { /** * The session for recording. - * <p> - * The caller is responsible for releasing the session when the error occurs. + * + * <p>The caller is responsible for releasing the session when the error occurs. */ public class RecordingSession { private final String mInputId; @@ -391,8 +392,12 @@ public class InputSessionManager { private TvRecordingClient mClient; private boolean mTuned; - RecordingSession(String inputId, String tag, RecordingCallback callback, - Handler handler, long endTimeMs) { + RecordingSession( + String inputId, + String tag, + RecordingCallback callback, + Handler handler, + long endTimeMs) { mInputId = inputId; mCallback = callback; mHandler = handler; @@ -402,83 +407,90 @@ public class InputSessionManager { void release() { if (DEBUG) Log.d(TAG, "Release of recording session requested."); - runOnHandler(mMainThreadHandler, new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "Releasing of recording session."); - mTuned = false; - mClient.release(); - mClient = null; - for (TvViewSession session : mTvViewSessions) { - if (DEBUG) { - Log.d(TAG, "Finding TvView sessions for retune: {tuned=" - + session.mTuned + ", inputId=" + session.mInputId - + ", session=" + session + "}"); - } - if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { - session.retune(); - break; + runOnHandler( + mMainThreadHandler, + new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Releasing of recording session."); + mTuned = false; + mClient.release(); + mClient = null; + for (TvViewSession session : mTvViewSessions) { + if (DEBUG) { + Log.d( + TAG, + "Finding TvView sessions for retune: {tuned=" + + session.mTuned + + ", inputId=" + + session.mInputId + + ", session=" + + session + + "}"); + } + if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { + session.retune(); + break; + } + } } - } - } - }); + }); } - /** - * Tunes to the channel for recording. - */ + /** Tunes to the channel for recording. */ public void tune(String inputId, Uri channelUri) { - runOnHandler(mMainThreadHandler, new Runnable() { - @Override - public void run() { - int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - if (input == null || !input.canRecord() - || input.getTunerCount() <= tunedRecordingSessionCount) { - runOnHandler(mHandler, new Runnable() { - @Override - public void run() { - mCallback.onConnectionFailed(inputId); + runOnHandler( + mMainThreadHandler, + new Runnable() { + @Override + public void run() { + int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + if (input == null + || !input.canRecord() + || input.getTunerCount() <= tunedRecordingSessionCount) { + runOnHandler( + mHandler, + new Runnable() { + @Override + public void run() { + mCallback.onConnectionFailed(inputId); + } + }); + return; } - }); - return; - } - mTuned = true; - int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); - if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0 - && tunedRecordingSessionCount + tunedTuneSessionCount - >= input.getTunerCount()) { - for (TvViewSession session : mTvViewSessions) { - if (session.mTuned && Objects.equals(session.mInputId, inputId) - && !isTunedForRecording(session.mChannelUri)) { - session.resetByRecording(); - break; + mTuned = true; + int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); + if (!isTunedForTvView(channelUri) + && tunedTuneSessionCount > 0 + && tunedRecordingSessionCount + tunedTuneSessionCount + >= input.getTunerCount()) { + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned + && Objects.equals(session.mInputId, inputId) + && !isTunedForRecording(session.mChannelUri)) { + session.resetByRecording(); + break; + } + } } + mChannelUri = channelUri; + mClient.tune(inputId, channelUri); } - } - mChannelUri = channelUri; - mClient.tune(inputId, channelUri); - } - }); + }); } - /** - * Starts recording. - */ + /** Starts recording. */ public void startRecording(Uri programHintUri) { mClient.startRecording(programHintUri); } - /** - * Stops recording. - */ + /** Stops recording. */ public void stopRecording() { mClient.stopRecording(); } - /** - * Sets recording session's ending time. - */ + /** Sets recording session's ending time. */ public void setEndTimeMs(long endTimeMs) { mEndTimeMs = endTimeMs; } @@ -555,9 +567,7 @@ public class InputSessionManager { } } - /** - * Called when the {@link TvView} channel is changed. - */ + /** Called when the {@link TvView} channel is changed. */ public interface OnTvViewChannelChangeListener { void onTvViewChannelChange(@Nullable Uri channelUri); } diff --git a/src/com/android/tv/LauncherActivity.java b/src/com/android/tv/LauncherActivity.java index e03952da..679d612d 100644 --- a/src/com/android/tv/LauncherActivity.java +++ b/src/com/android/tv/LauncherActivity.java @@ -26,8 +26,8 @@ import android.util.Log; /** * An activity to launch a new activity. * - * <p>In the case when {@link MainActivity} starts a new activity using - * {@link Activity#startActivity} or {@link Activity#startActivityForResult}, Live TV app is + * <p>In the case when {@link MainActivity} starts a new activity using {@link + * Activity#startActivity} or {@link Activity#startActivityForResult}, Live TV app is * terminated if the new activity crashes. That's because the {@link android.app.ActivityManager} * terminates the activity which is just below the crashed activity in the activity stack. To avoid * this, we need to locate an additional activity between these activities in the activity stack. @@ -35,8 +35,7 @@ import android.util.Log; public class LauncherActivity extends Activity { private static final String TAG = "LauncherActivity"; - public static final String ERROR_MESSAGE - = "com.android.tv.LauncherActivity.ErrorMessage"; + public static final String ERROR_MESSAGE = "com.android.tv.LauncherActivity.ErrorMessage"; private static final int REQUEST_CODE_DEFAULT = 0; private static final int REQUEST_START_ACTIVITY = 100; @@ -45,34 +44,16 @@ public class LauncherActivity extends Activity { private static final String EXTRA_REQUEST_RESULT = "com.android.tv.LauncherActivity.REQUEST_RESULT"; - /** - * Starts an activity by calling {@link Activity#startActivity}. - */ + /** Starts an activity by calling {@link Activity#startActivity}. */ public static void startActivitySafe(Activity baseActivity, Intent intentToLaunch) { // To avoid the app termination when the new activity crashes, LauncherActivity should be // started by calling startActivityForResult(). - baseActivity.startActivityForResult(createIntent(baseActivity, intentToLaunch, false), - REQUEST_CODE_DEFAULT); + baseActivity.startActivityForResult( + createIntent(baseActivity, intentToLaunch, false), REQUEST_CODE_DEFAULT); } - /** - * Starts an activity by calling {@link Activity#startActivityForResult}. - * - * <p>Note: {@code requestCode} should not be 0. The value is reserved for internal use. - */ - public static void startActivityForResultSafe(Activity baseActivity, Intent intentToLaunch, - int requestCode) { - if (requestCode == REQUEST_CODE_DEFAULT) { - throw new IllegalArgumentException("requestCode should not be 0."); - } - // To avoid the app termination when the new activity crashes, LauncherActivity should be - // started by calling startActivityForResult(). - baseActivity.startActivityForResult(createIntent(baseActivity, intentToLaunch, true), - requestCode); - } - - private static Intent createIntent(Context context, Intent intentToLaunch, - boolean requestResult) { + private static Intent createIntent( + Context context, Intent intentToLaunch, boolean requestResult) { Intent intent = new Intent(context, LauncherActivity.class); intent.putExtra(EXTRA_INTENT, intentToLaunch); if (requestResult) { @@ -98,8 +79,7 @@ public class LauncherActivity extends Activity { } } catch (ActivityNotFoundException e) { Log.w(TAG, "Activity not found for " + intent); - intent.putExtra(ERROR_MESSAGE, - getResources().getString(R.string.msg_missing_app)); + intent.putExtra(ERROR_MESSAGE, getResources().getString(R.string.msg_missing_app)); setResult(Activity.RESULT_CANCELED, intent); finish(); } diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index ed5f79a1..94a86cce 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -17,9 +17,12 @@ package com.android.tv; import android.app.Activity; +import android.app.PendingIntent; +import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; @@ -62,26 +65,31 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.Toast; - import com.android.tv.analytics.SendChannelStatusRunnable; import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; import com.android.tv.common.BuildConfig; -import com.android.tv.common.MemoryManageable; +import com.android.tv.common.CommonPreferences; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.memory.MemoryManageable; import com.android.tv.common.ui.setup.OnActionClickListener; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.ContentUriUtils; +import com.android.tv.common.util.Debug; +import com.android.tv.common.util.DurationTimer; +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.common.util.SystemProperties; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelImpl; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; -import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; @@ -97,15 +105,10 @@ import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.perf.EventNames; import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.StubPerformanceMonitor; import com.android.tv.perf.TimerEvent; import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.recommendation.NotificationService; import com.android.tv.search.ProgramGuideSearchFragment; -import com.android.tv.tuner.TunerInputController; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.setup.TunerSetupActivity; -import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.ui.ChannelBannerView; import com.android.tv.ui.InputBannerView; import com.android.tv.ui.KeypadChannelSwitchView; @@ -124,21 +127,18 @@ import com.android.tv.ui.sidepanel.MultiAudioFragment; import com.android.tv.ui.sidepanel.SettingsFragment; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment; -import com.android.tv.util.AccountHelper; +import com.android.tv.util.AsyncDbTask; import com.android.tv.util.CaptionSettings; -import com.android.tv.util.Debug; -import com.android.tv.util.DurationTimer; -import com.android.tv.util.ImageCache; import com.android.tv.util.OnboardingUtils; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.SetupUtils; -import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvSettings; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; import com.android.tv.util.ViewCache; +import com.android.tv.util.account.AccountHelper; +import com.android.tv.util.images.ImageCache; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -148,19 +148,23 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -/** - * The main activity for the Live TV app. - */ +/** The main activity for the Live TV app. */ public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener { private static final String TAG = "MainActivity"; private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) - @IntDef({KEY_EVENT_HANDLER_RESULT_PASSTHROUGH, KEY_EVENT_HANDLER_RESULT_NOT_HANDLED, - KEY_EVENT_HANDLER_RESULT_HANDLED, KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY}) + @IntDef({ + KEY_EVENT_HANDLER_RESULT_PASSTHROUGH, + KEY_EVENT_HANDLER_RESULT_NOT_HANDLED, + KEY_EVENT_HANDLER_RESULT_HANDLED, + KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY + }) public @interface KeyHandlerResultType {} + public static final int KEY_EVENT_HANDLER_RESULT_PASSTHROUGH = 0; public static final int KEY_EVENT_HANDLER_RESULT_NOT_HANDLED = 1; public static final int KEY_EVENT_HANDLER_RESULT_HANDLED = 2; @@ -171,12 +175,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private static final float FRAME_RATE_FOR_FILM = 23.976f; private static final float FRAME_RATE_EPSILON = 0.1f; - private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; // Tracker screen names. public static final String SCREEN_NAME = "Main"; + private static final String SCREEN_PIP = "PIP"; private static final String SCREEN_BEHIND_NAME = "Behind"; private static final float REFRESH_RATE_EPSILON = 0.01f; @@ -196,8 +200,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_WINDOW); } - private static final IntentFilter SYSTEM_INTENT_FILTER = new IntentFilter(); + static { SYSTEM_INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED); SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF); @@ -206,6 +210,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; + private static final int REQUEST_CODE_NOW_PLAYING = 2; private static final String KEY_INIT_CHANNEL_ID = "com.android.tv.init_channel_id"; @@ -240,14 +245,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private final DurationTimer mTuneDurationTimer = new DurationTimer(); private DvrManager mDvrManager; private ConflictChecker mDvrConflictChecker; + private SetupUtils mSetupUtils; private View mContentView; private TunableTvView mTvView; private Bundle mTuneParams; - @Nullable - private Uri mInitChannelUri; - @Nullable - private String mParentInputIdWhenScreenOff; + @Nullable private Uri mInitChannelUri; + @Nullable private String mParentInputIdWhenScreenOff; private boolean mScreenOffIntentReceived; private boolean mShowProgramGuide; private boolean mShowSelectInputView; @@ -274,6 +278,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private boolean mOtherActivityLaunched; private PerformanceMonitor mPerformanceMonitor; + private boolean mIsInPIPMode; private boolean mIsFilmModeSet; private float mDefaultRefreshRate; @@ -302,69 +307,74 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private final Handler mHandler = new MainActivityHandler(this); private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>(); - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - switch (intent.getAction()) { - case Intent.ACTION_SCREEN_OFF: - if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_OFF"); - // We need to stop TvView, when the screen is turned off. If not and TIS uses - // MediaPlayer, a device may not go to the sleep mode and audio can be heard, - // because MediaPlayer keeps playing media by its wake lock. - mScreenOffIntentReceived = true; - markCurrentChannelDuringScreenOff(); - stopAll(true); - break; - case Intent.ACTION_SCREEN_ON: - if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON"); - if (!mActivityResumed && mVisibleBehind) { - // ACTION_SCREEN_ON is usually called after onResume. But, if media is - // played under launcher with requestVisibleBehind(true), onResume will - // not be called. In this case, we need to resume TvView explicitly. - resumeTvIfNeeded(); - } - break; - case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED: - if (DEBUG) Log.d(TAG, "Received parental control settings change"); - applyParentalControlSettings(); - checkChannelLockNeeded(mTvView, null); - break; - case Intent.ACTION_TIME_CHANGED: - // Re-tune the current channel to prevent incorrect behavior of trick-play. - // See: b/37393628 - if (mChannelTuner.getCurrentChannel() != null) { - tune(true); + private final BroadcastReceiver mBroadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case Intent.ACTION_SCREEN_OFF: + if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_OFF"); + // We need to stop TvView, when the screen is turned off. If not and TIS + // uses MediaPlayer, a device may not go to the sleep mode and audio + // can be heard, because MediaPlayer keeps playing media by its wake + // lock. + mScreenOffIntentReceived = true; + markCurrentChannelDuringScreenOff(); + stopAll(true); + break; + case Intent.ACTION_SCREEN_ON: + if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON"); + if (!mActivityResumed && mVisibleBehind) { + // ACTION_SCREEN_ON is usually called after onResume. But, if media + // is played under launcher with requestVisibleBehind(true), + // onResume will not be called. In this case, we need to resume + // TvView explicitly. + resumeTvIfNeeded(); + } + break; + case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED: + if (DEBUG) Log.d(TAG, "Received parental control settings change"); + applyParentalControlSettings(); + checkChannelLockNeeded(mTvView, null); + break; + case Intent.ACTION_TIME_CHANGED: + // Re-tune the current channel to prevent incorrect behavior of + // trick-play. + // See: b/37393628 + if (mChannelTuner.getCurrentChannel() != null) { + tune(true); + } + break; + default: // fall out } - break; - } - } - }; + } + }; private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener = new OnCurrentProgramUpdatedListener() { - @Override - public void onCurrentProgramUpdated(long channelId, Program program) { - // Do not update channel banner by this notification - // when the time shifting is available. - if (mTimeShiftManager.isAvailable()) { - return; - } - Channel channel = mTvView.getCurrentChannel(); - if (channel != null && channel.getId() == channelId) { - mOverlayManager.updateChannelBannerAndShowIfNeeded( - TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - mMediaSessionWrapper.update(mTvView.isBlocked(), channel, program); - } - } - }; + @Override + public void onCurrentProgramUpdated(long channelId, Program program) { + // Do not update channel banner by this notification + // when the time shifting is available. + if (mTimeShiftManager.isAvailable()) { + return; + } + Channel channel = mTvView.getCurrentChannel(); + if (channel != null && channel.getId() == channelId) { + mOverlayManager.updateChannelBannerAndShowIfNeeded( + TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + mMediaSessionWrapper.update(mTvView.isBlocked(), channel, program); + } + } + }; private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { @Override public void onLoadFinished() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "MainActivity.mChannelTunerListener.onLoadFinished"); - SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable(); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log("MainActivity.mChannelTunerListener.onLoadFinished"); + mSetupUtils.markNewChannelsBrowsable(); if (mActivityResumed) { resumeTvIfNeeded(); } @@ -389,30 +399,35 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP public void onChannelChanged(Channel previousChannel, Channel currentChannel) {} }; - private final Runnable mRestoreMainViewRunnable = new Runnable() { - @Override - public void run() { - restoreMainTvView(); - } - }; + private final Runnable mRestoreMainViewRunnable = + new Runnable() { + @Override + public void run() { + restoreMainTvView(); + } + }; private ProgramGuideSearchFragment mSearchFragment; - private final TvInputCallback mTvInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - if (Features.TUNER.isEnabled(MainActivity.this) && mTunerInputId.equals(inputId) - && TunerPreferences.shouldShowSetupActivity(MainActivity.this)) { - Intent intent = TunerSetupActivity.createSetupActivity(MainActivity.this); - startActivity(intent); - TunerPreferences.setShouldShowSetupActivity(MainActivity.this, false); - SetupUtils.getInstance(MainActivity.this).markAsKnownInput(mTunerInputId); - } - } - }; + private final TvInputCallback mTvInputCallback = + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (TvFeatures.TUNER.isEnabled(MainActivity.this) + && mTunerInputId.equals(inputId) + && CommonPreferences.shouldShowSetupActivity(MainActivity.this)) { + Intent intent = + TvSingletons.getSingletons(MainActivity.this) + .getTunerSetupIntent(MainActivity.this); + startActivity(intent); + CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false); + mSetupUtils.markAsKnownInput(mTunerInputId); + } + } + }; private void applyParentalControlSettings() { - boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings() - .isParentalControlsEnabled(); + boolean parentalControlEnabled = + mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled(); mTvView.onParentalControlChanged(parentalControlEnabled); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ChannelPreviewUpdater.getInstance(this).updatePreviewDataForChannelsImmediately(); @@ -421,7 +436,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP @Override protected void onCreate(Bundle savedInstanceState) { - TimerEvent timer = StubPerformanceMonitor.startBootstrapTimer(); + mAccessibilityManager = + (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); + TvSingletons tvSingletons = TvSingletons.getSingletons(this); + mPerformanceMonitor = tvSingletons.getPerformanceMonitor(); + TimerEvent timer = mPerformanceMonitor.startTimer(); DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER); if (!startUpDebugTimer.isStarted() || startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) { @@ -430,30 +449,33 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP startUpDebugTimer.start(); } startUpDebugTimer.log("MainActivity.onCreate"); - if (DEBUG) Log.d(TAG,"onCreate()"); - TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) { + Log.d(TAG, "onCreate()"); + } + Starter.start(this); super.onCreate(savedInstanceState); - ApplicationSingletons applicationSingletons = TvApplication.getSingletons(this); - if (!applicationSingletons.getTvInputManagerHelper().hasTvInputManager()) { + if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); finishAndRemoveTask(); return; } - mPerformanceMonitor = applicationSingletons.getPerformanceMonitor(); + mPerformanceMonitor = tvSingletons.getPerformanceMonitor(); + mSetupUtils = tvSingletons.getSetupUtils(); TvApplication tvApplication = (TvApplication) getApplication(); mChannelDataManager = tvApplication.getChannelDataManager(); // In API 23, TvContract.isChannelUriForPassthroughInput is hidden. boolean isPassthroughInput = TvContract.isChannelUriForPassthroughInput(getIntent().getData()); - boolean tuneToPassthroughInput = Intent.ACTION_VIEW.equals(getIntent().getAction()) - && isPassthroughInput; - boolean channelLoadedAndNoChannelAvailable = mChannelDataManager.isDbLoadFinished() - && mChannelDataManager.getChannelCount() <= 0; + boolean tuneToPassthroughInput = + Intent.ACTION_VIEW.equals(getIntent().getAction()) && isPassthroughInput; + boolean channelLoadedAndNoChannelAvailable = + mChannelDataManager.isDbLoadFinished() + && mChannelDataManager.getChannelCount() <= 0; if ((OnboardingUtils.isFirstRunWithCurrentVersion(this) - || channelLoadedAndNoChannelAvailable) + || channelLoadedAndNoChannelAvailable) && !tuneToPassthroughInput - && !TvCommonUtils.isRunningInTest()) { + && !CommonUtils.isRunningInTest()) { startOnboardingActivity(); return; } @@ -462,31 +484,38 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view); mTvView.initialize(mProgramDataManager, mTvInputManagerHelper); - mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() { - @Override - public boolean onUnhandledInputEvent(InputEvent event) { - if (isKeyEventBlocked()) { - return true; - } - if (event instanceof KeyEvent) { - KeyEvent keyEvent = (KeyEvent) event; - if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.isLongPress()) { - if (onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { + mTvView.setOnUnhandledInputEventListener( + new OnUnhandledInputEventListener() { + @Override + public boolean onUnhandledInputEvent(InputEvent event) { + if (DEBUG) { + Log.d(TAG, "onUnhandledInputEvent " + event); + } + if (isKeyEventBlocked()) { return true; } + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN + && keyEvent.isLongPress()) { + if (onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { + return true; + } + } + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + return onKeyUp(keyEvent.getKeyCode(), keyEvent); + } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyEvent.getKeyCode(), keyEvent); + } + } + return false; } - if (keyEvent.getAction() == KeyEvent.ACTION_UP) { - return onKeyUp(keyEvent.getKeyCode(), keyEvent); - } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { - return onKeyDown(keyEvent.getKeyCode(), keyEvent); - } - } - return false; - } - }); + }); + mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null)); long channelId = Utils.getLastWatchedChannelId(this); String inputId = Utils.getLastWatchedTunerInputId(this); - if (!isPassthroughInput && inputId != null + if (!isPassthroughInput + && inputId != null && channelId != Channel.INVALID_ID) { mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId)); } @@ -496,12 +525,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); } mTracker = tvApplication.getTracker(); - if (Features.TUNER.isEnabled(this)) { + if (TvFeatures.TUNER.isEnabled(this)) { mTvInputManagerHelper.addCallback(mTvInputCallback); } - mTunerInputId = TunerTvInputService.getInputId(this); - mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, - mOnCurrentProgramUpdatedListener); + mTunerInputId = tvSingletons.getEmbeddedTunerInputId(); + mProgramDataManager.addOnCurrentProgramUpdatedListener( + Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); mProgramDataManager.setPrefetchEnabled(true); mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper); mChannelTuner.addListener(mChannelTunerListener); @@ -512,91 +541,126 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (CommonFeatures.DVR.isEnabled(this)) { mDvrManager = tvApplication.getDvrManager(); } - mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, mTracker, - new OnCurrentProgramUpdatedListener() { - @Override - public void onCurrentProgramUpdated(long channelId, Program program) { - mMediaSessionWrapper.update(mTvView.isBlocked(), getCurrentChannel(), - program); - switch (mTimeShiftManager.getLastActionId()) { - case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND: - case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD: - case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS: - case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT: - mOverlayManager.updateChannelBannerAndShowIfNeeded( - TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW); - break; - case TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE: - case TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY: - default: - mOverlayManager.updateChannelBannerAndShowIfNeeded( - TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); - break; - } - } - }); + mTimeShiftManager = + new TimeShiftManager( + this, + mTvView, + mProgramDataManager, + mTracker, + new OnCurrentProgramUpdatedListener() { + @Override + public void onCurrentProgramUpdated(long channelId, Program program) { + mMediaSessionWrapper.update( + mTvView.isBlocked(), getCurrentChannel(), program); + switch (mTimeShiftManager.getLastActionId()) { + case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND: + case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD: + case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS: + case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT: + mOverlayManager.updateChannelBannerAndShowIfNeeded( + TvOverlayManager + .UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW); + break; + case TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE: + case TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY: + default: + mOverlayManager.updateChannelBannerAndShowIfNeeded( + TvOverlayManager + .UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO); + break; + } + } + }); DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); mDefaultRefreshRate = display.getRefreshRate(); if (!PermissionUtils.hasAccessWatchedHistory(this)) { - WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager( - getApplicationContext()); + WatchedHistoryManager watchedHistoryManager = + new WatchedHistoryManager(getApplicationContext()); watchedHistoryManager.start(); mTvView.setWatchedHistoryManager(watchedHistoryManager); } - mTvViewUiManager = new TvViewUiManager(this, mTvView, - (FrameLayout) findViewById(android.R.id.content), mTvOptionsManager); + mTvViewUiManager = + new TvViewUiManager( + this, + mTvView, + (FrameLayout) findViewById(android.R.id.content), + mTvOptionsManager); mContentView = findViewById(android.R.id.content); ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container); - ChannelBannerView channelBannerView = (ChannelBannerView) getLayoutInflater().inflate( - R.layout.channel_banner, sceneContainer, false); - KeypadChannelSwitchView keypadChannelSwitchView = (KeypadChannelSwitchView) - getLayoutInflater().inflate(R.layout.keypad_channel_switch, sceneContainer, false); - InputBannerView inputBannerView = (InputBannerView) getLayoutInflater() - .inflate(R.layout.input_banner, sceneContainer, false); - SelectInputView selectInputView = (SelectInputView) getLayoutInflater() - .inflate(R.layout.select_input, sceneContainer, false); - selectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() { - @Override - public void onTunerInputSelected() { - Channel currentChannel = mChannelTuner.getCurrentChannel(); - if (currentChannel != null && !currentChannel.isPassthrough()) { - hideOverlays(); - } else { - tuneToLastWatchedChannelForTunerInput(); - } - } + ChannelBannerView channelBannerView = + (ChannelBannerView) + getLayoutInflater().inflate(R.layout.channel_banner, sceneContainer, false); + KeypadChannelSwitchView keypadChannelSwitchView = + (KeypadChannelSwitchView) + getLayoutInflater() + .inflate(R.layout.keypad_channel_switch, sceneContainer, false); + InputBannerView inputBannerView = + (InputBannerView) + getLayoutInflater().inflate(R.layout.input_banner, sceneContainer, false); + SelectInputView selectInputView = + (SelectInputView) + getLayoutInflater().inflate(R.layout.select_input, sceneContainer, false); + selectInputView.setOnInputSelectedCallback( + new OnInputSelectedCallback() { + @Override + public void onTunerInputSelected() { + Channel currentChannel = mChannelTuner.getCurrentChannel(); + if (currentChannel != null && !currentChannel.isPassthrough()) { + hideOverlays(); + } else { + tuneToLastWatchedChannelForTunerInput(); + } + } - @Override - public void onPassthroughInputSelected(@NonNull TvInputInfo input) { - Channel currentChannel = mChannelTuner.getCurrentChannel(); - String currentInputId = currentChannel == null ? null : currentChannel.getInputId(); - if (TextUtils.equals(input.getId(), currentInputId)) { - hideOverlays(); - } else { - tuneToChannel(Channel.createPassthroughChannel(input.getId())); - } - } + @Override + public void onPassthroughInputSelected(@NonNull TvInputInfo input) { + Channel currentChannel = mChannelTuner.getCurrentChannel(); + String currentInputId = + currentChannel == null ? null : currentChannel.getInputId(); + if (TextUtils.equals(input.getId(), currentInputId)) { + hideOverlays(); + } else { + tuneToChannel(ChannelImpl.createPassthroughChannel(input.getId())); + } + } - private void hideOverlays() { - getOverlayManager().hideOverlays( - TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); - } - }); + private void hideOverlays() { + getOverlayManager() + .hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + } + }); mSearchFragment = new ProgramGuideSearchFragment(); - mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mTvOptionsManager, - keypadChannelSwitchView, channelBannerView, inputBannerView, - selectInputView, sceneContainer, mSearchFragment); + mOverlayManager = + new TvOverlayManager( + this, + mChannelTuner, + mTvView, + mTvOptionsManager, + keypadChannelSwitchView, + channelBannerView, + inputBannerView, + selectInputView, + sceneContainer, + mSearchFragment); + mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager); mAudioManagerHelper = new AudioManagerHelper(this, mTvView); - mMediaSessionWrapper = new MediaSessionWrapper(this); + Intent nowPlayingIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = + PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0); + mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent); mTvViewUiManager.restoreDisplayMode(false); if (!handleIntent(getIntent())) { @@ -604,18 +668,21 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; } - mAccessibilityManager = - (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); - mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1), - new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper), null); + mSendConfigInfoRecurringRunner = + new RecurringRunner( + this, + TimeUnit.DAYS.toMillis(1), + new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper), + null); mSendConfigInfoRecurringRunner.start(); - mChannelStatusRecurringRunner = SendChannelStatusRunnable - .startChannelStatusRecurringRunner(this, mTracker, mChannelDataManager); + mChannelStatusRecurringRunner = + SendChannelStatusRunnable.startChannelStatusRecurringRunner( + this, mTracker, mChannelDataManager); // To avoid not updating Rating systems when changing language. mTvInputManagerHelper.getContentRatingsManager().update(); if (CommonFeatures.DVR.isEnabled(this) - && Features.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) { + && TvFeatures.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) { mDvrConflictChecker = new ConflictChecker(this); } initForTest(); @@ -632,13 +699,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); float density = getResources().getDisplayMetrics().density; - mTvViewUiManager.onConfigurationChanged((int) (newConfig.screenWidthDp * density), + mTvViewUiManager.onConfigurationChanged( + (int) (newConfig.screenWidthDp * density), (int) (newConfig.screenHeightDp * density)); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Start reload of dependent data @@ -650,14 +718,18 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP finish(); startActivity(intent); } else { - Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied, - Toast.LENGTH_LONG).show(); + Toast.makeText( + this, + R.string.msg_read_tv_listing_permission_denied, + Toast.LENGTH_LONG) + .show(); finish(); } } } - @BlockScreenType private int getDesiredBlockScreenType() { + @BlockScreenType + private int getDesiredBlockScreenType() { if (!mActivityResumed) { return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI; } @@ -689,7 +761,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP @Override protected void onNewIntent(Intent intent) { - if (DEBUG) Log.d(TAG,"onNewIntent(): " + intent); + if (DEBUG) { + Log.d(TAG, "onNewIntent(): " + intent); + } if (mOverlayManager == null) { // It's called before onCreate. The intent will be handled at onCreate. b/30725058 return; @@ -705,7 +779,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP @Override protected void onStart() { TimerEvent timer = mPerformanceMonitor.startTimer(); - if (DEBUG) Log.d(TAG,"onStart()"); + if (DEBUG) { + Log.d(TAG, "onStart()"); + } super.onStart(); mScreenOffIntentReceived = false; mActivityStarted = true; @@ -720,9 +796,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); startService(notificationIntent); } - TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this); - - EpgFetcher.getInstance(this).fetchImmediatelyIfNeeded(); + TvSingletons singletons = TvSingletons.getSingletons(this); + singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this); + singletons.getEpgFetcher().fetchImmediatelyIfNeeded(); mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART); } @@ -732,10 +808,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start"); if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); + mIsInPIPMode = false; if (!PermissionUtils.hasAccessAllEpg(this) && checkSelfPermission(PERMISSION_READ_TV_LISTINGS) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {PERMISSION_READ_TV_LISTINGS}, PERMISSIONS_REQUEST_READ_TV_LISTINGS); } mTracker.sendScreenView(SCREEN_NAME); @@ -755,19 +833,20 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP Set<String> failedScheduledRecordingInfoSet = Utils.getFailedScheduledRecordingInfoSet(getApplicationContext()); if (Utils.hasRecordingFailedReason( - getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE) + getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE) && !failedScheduledRecordingInfoSet.isEmpty()) { - runAfterAttachedToWindow(new Runnable() { - @Override - public void run() { - DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this, - failedScheduledRecordingInfoSet); - } - }); + runAfterAttachedToWindow( + new Runnable() { + @Override + public void run() { + DvrUiHelper.showDvrInsufficientSpaceErrorDialog( + MainActivity.this, failedScheduledRecordingInfoSet); + } + }); } if (mChannelTuner.areAllChannelsLoaded()) { - SetupUtils.getInstance(this).markNewChannelsBrowsable(); + mSetupUtils.markNewChannelsBrowsable(); resumeTvIfNeeded(); } mOverlayManager.showMenuWithTimeShiftPauseIfNeeded(); @@ -779,28 +858,29 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mInputToSetUp = null; } else if (mShowProgramGuide) { mShowProgramGuide = false; - mHandler.post(new Runnable() { - // This will delay the start of the animation until after the Live Channel app is - // shown. Without this the animation is completed before it is actually visible on - // the screen. - @Override - public void run() { - mOverlayManager.showProgramGuide(); - } - }); + // This will delay the start of the animation until after the Live Channel app is + // shown. Without this the animation is completed before it is actually visible on + // the screen. + mHandler.post( + new Runnable() { + @Override + public void run() { + mOverlayManager.showProgramGuide(); + } + }); } else if (mShowSelectInputView) { mShowSelectInputView = false; - mHandler.post(new Runnable() { - // mShowSelectInputView is true when the activity is started/resumed because the - // TV_INPUT button was pressed in a different app. - // This will delay the start of the animation until after the Live Channel app is - // shown. Without this the animation is completed before it is actually visible on - // the screen. - @Override - public void run() { - mOverlayManager.showSelectInputView(); - } - }); + // mShowSelectInputView is true when the activity is started/resumed because the + // TV_INPUT button was pressed in a different app. This will delay the start of + // the animation until after the Live Channel app is shown. Without this the + // animation is completed before it is actually visible on the screen. + mHandler.post( + new Runnable() { + @Override + public void run() { + mOverlayManager.showSelectInputView(); + } + }); } if (mDvrConflictChecker != null) { mDvrConflictChecker.start(); @@ -823,25 +903,26 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mShowLockedChannelsTemporarily = false; mShouldTuneToTunerChannel = false; if (!mVisibleBehind) { - mAudioManagerHelper.abandonAudioFocus(); - mMediaSessionWrapper.setPlaybackState(false); - mTracker.sendScreenView(""); + if (mIsInPIPMode) { + mTracker.sendScreenView(SCREEN_PIP); + } else { + mTracker.sendScreenView(""); + mAudioManagerHelper.abandonAudioFocus(); + mMediaSessionWrapper.setPlaybackState(false); + } } else { mTracker.sendScreenView(SCREEN_BEHIND_NAME); } + TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this); super.onPause(); } - /** - * Returns true if {@link #onResume} is called and {@link #onPause} is not called yet. - */ + /** Returns true if {@link #onResume} is called and {@link #onPause} is not called yet. */ public boolean isActivityResumed() { return mActivityResumed; } - /** - * Returns true if {@link #onStart} is called and {@link #onStop} is not called yet. - */ + /** Returns true if {@link #onStart} is called and {@link #onStop} is not called yet. */ public boolean isActivityStarted() { return mActivityStarted; } @@ -867,12 +948,14 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mTvView.unblockContent(unblockedRating); break; case PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN: - mOverlayManager.getSideFragmentManager() + mOverlayManager + .getSideFragmentManager() .show(new ParentalControlsFragment(), false); - // Pass through. + // fall through. case PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN: mOverlayManager.getSideFragmentManager().showSidePanel(true); break; + default: // fall out } } else if (type == PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN) { mOverlayManager.getSideFragmentManager().hideAll(false); @@ -881,7 +964,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private void resumeTvIfNeeded() { if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()"); - if (!mTvView.isPlaying() || mInitChannelUri != null + if (!mTvView.isPlaying() + || mInitChannelUri != null || (mShouldTuneToTunerChannel && mChannelTuner.isCurrentChannelPassthrough())) { if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) { // The target input may not be ready yet, especially, just after screen on. @@ -917,9 +1001,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // is playing, we stop the passthrough TV input. stopTv(); } - SoftPreconditions.checkState(TvContract.isChannelUriForPassthroughInput(channelUri) - || mChannelTuner.areAllChannelsLoaded(), - TAG, "startTV assumes that ChannelDataManager is already loaded."); + SoftPreconditions.checkState( + TvContract.isChannelUriForPassthroughInput(channelUri) + || mChannelTuner.areAllChannelsLoaded(), + TAG, + "startTV assumes that ChannelDataManager is already loaded."); if (mTvView.isPlaying()) { // TV has already started. if (channelUri == null || channelUri.equals(mChannelTuner.getCurrentChannelUri())) { @@ -945,15 +1031,19 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0)); } else { if (TvContract.isChannelUriForPassthroughInput(channelUri)) { - Channel channel = Channel.createPassthroughChannel(channelUri); + ChannelImpl channel = ChannelImpl.createPassthroughChannel(channelUri); mChannelTuner.moveToChannel(channel); } else { long channelId = ContentUris.parseId(channelUri); Channel channel = mChannelDataManager.getChannel(channelId); if (channel == null || !mChannelTuner.moveToChannel(channel)) { mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0)); - Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. " - + "The first channel will be tuned to."); + Log.w( + TAG, + "The requested channel (id=" + + channelId + + ") doesn't exist. " + + "The first channel will be tuned to."); } } } @@ -985,9 +1075,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP super.onStop(); } - /** - * Handles screen off to keep the current channel for next screen on. - */ + /** Handles screen off to keep the current channel for next screen on. */ private void markCurrentChannelDuringScreenOff() { mInitChannelUri = mChannelTuner.getCurrentChannelUri(); if (mChannelTuner.isCurrentChannelPassthrough()) { @@ -1015,7 +1103,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP * @param calledByPopup If true, startSetupActivity is invoked from the setup fragment. */ public void startSetupActivity(TvInputInfo input, boolean calledByPopup) { - Intent intent = TvCommonUtils.createSetupIntent(input); + Intent intent = CommonUtils.createSetupIntent(input); if (intent == null) { Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show(); return; @@ -1038,16 +1126,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); } catch (ActivityNotFoundException e) { mInputIdUnderSetup = null; - Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity, - input.loadLabel(this)), Toast.LENGTH_SHORT).show(); + Toast.makeText( + this, + getString( + R.string.msg_unable_to_start_setup_activity, + input.loadLabel(this)), + Toast.LENGTH_SHORT) + .show(); return; } if (calledByPopup) { - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + mOverlayManager.hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); } else { - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY); + mOverlayManager.hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY); } } @@ -1060,8 +1155,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP try { startActivitySafe(intent); } catch (ActivityNotFoundException e) { - Toast.makeText(this, getString(R.string.msg_unable_to_start_system_captioning_settings), - Toast.LENGTH_SHORT).show(); + Toast.makeText( + this, + getString(R.string.msg_unable_to_start_system_captioning_settings), + Toast.LENGTH_SHORT) + .show(); } } @@ -1085,16 +1183,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return mTimeShiftManager; } - /** - * Returns the instance of {@link TvOverlayManager}. - */ + /** Returns the instance of {@link TvOverlayManager}. */ public TvOverlayManager getOverlayManager() { return mOverlayManager; } - /** - * Returns the {@link ConflictChecker}. - */ + /** Returns the {@link ConflictChecker}. */ @Nullable public ConflictChecker getDvrConflictChecker() { return mDvrConflictChecker; @@ -1109,9 +1203,10 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } /** - * Returns the current program which the user is watching right now.<p> + * Returns the current program which the user is watching right now. * - * It might be a live program. If the time shifting is available, it can be a past program, too. + * <p>It might be a live program. If the time shifting is available, it can be a past program, + * too. */ public Program getCurrentProgram() { if (!isChannelChangeKeyDownReceived() && mTimeShiftManager.isAvailable()) { @@ -1122,9 +1217,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } /** - * Returns the current playing time in milliseconds.<p> + * Returns the current playing time in milliseconds. * - * If the time shifting is available, the time is the playing position of the program, + * <p>If the time shifting is available, the time is the playing position of the program, * otherwise, the system current time. */ public long getCurrentPlayingPosition() { @@ -1152,18 +1247,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP LauncherActivity.startActivitySafe(this, intent); } - /** - * Call {@link Activity#startActivityForResult} in a safe way. - * - * @see LauncherActivity - */ - private void startActivityForResultSafe(Intent intent, int requestCode) { - LauncherActivity.startActivityForResultSafe(this, intent, requestCode); - } - - /** - * Show settings fragment. - */ + /** Show settings fragment. */ public void showSettingsFragment() { if (!mChannelTuner.areAllChannelsLoaded()) { // Show ChannelSourcesFragment only if all the channels are loaded. @@ -1180,8 +1264,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP * It is called when shrunken TvView is desired, such as EditChannelFragment and * ChannelsLockedFragment. */ - public void startShrunkenTvView(boolean showLockedChannelsTemporarily, - boolean willMainViewBeTunerInput) { + public void startShrunkenTvView( + boolean showLockedChannelsTemporarily, boolean willMainViewBeTunerInput) { mChannelBeforeShrunkenTvView = mTvView.getCurrentChannel(); mWasChannelUnblockedBeforeShrunkenByUser = mIsCurrentChannelUnblockedByUser; mAllowedRatingBeforeShrunken = mLastAllowedRatingForCurrentChannel; @@ -1214,19 +1298,21 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // The current channel is mTvView.getCurrentChannel() and need to tune to the returnChannel. if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) { final Channel channel = returnChannel; - Runnable tuneAction = new Runnable() { - @Override - public void run() { - tuneToChannel(channel); - if (mChannelBeforeShrunkenTvView == null - || !mChannelBeforeShrunkenTvView.equals(channel)) { - Utils.setLastWatchedChannel(MainActivity.this, channel); - } - mIsCompletingShrunkenTvView = false; - mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser; - mTvView.setBlockScreenType(getDesiredBlockScreenType()); - } - }; + Runnable tuneAction = + new Runnable() { + @Override + public void run() { + tuneToChannel(channel); + if (mChannelBeforeShrunkenTvView == null + || !mChannelBeforeShrunkenTvView.equals(channel)) { + Utils.setLastWatchedChannel(MainActivity.this, channel); + } + mIsCompletingShrunkenTvView = false; + mIsCurrentChannelUnblockedByUser = + mWasChannelUnblockedBeforeShrunkenByUser; + mTvView.setBlockScreenType(getDesiredBlockScreenType()); + } + }; mTvViewUiManager.fadeOutTvView(tuneAction); // Will automatically fade-in when video becomes available. } else { @@ -1247,35 +1333,45 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP */ public boolean isScreenBlockedByResourceConflictOrParentalControl() { return mTvView.getVideoUnavailableReason() - == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE || mTvView.isBlocked(); + == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE + || mTvView.isBlocked(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_START_SETUP_ACTIVITY) { - if (resultCode == RESULT_OK) { - int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup); - String text; - if (count > 0) { - text = getResources().getQuantityString(R.plurals.msg_channel_added, - count, count); + switch (requestCode) { + case REQUEST_CODE_START_SETUP_ACTIVITY: + if (resultCode == RESULT_OK) { + int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup); + String text; + if (count > 0) { + text = + getResources() + .getQuantityString( + R.plurals.msg_channel_added, count, count); + } else { + text = getString(R.string.msg_no_channel_added); + } + Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show(); + mInputIdUnderSetup = null; + if (mChannelTuner.getCurrentChannel() == null) { + mChannelTuner.moveToAdjacentBrowsableChannel(true); + } + if (mTunePending) { + tune(true); + } } else { - text = getString(R.string.msg_no_channel_added); + mInputIdUnderSetup = null; } - Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show(); - mInputIdUnderSetup = null; - if (mChannelTuner.getCurrentChannel() == null) { - mChannelTuner.moveToAdjacentBrowsableChannel(true); + if (!mIsSetupActivityCalledByPopup) { + mOverlayManager.getSideFragmentManager().showSidePanel(false); } - if (mTunePending) { - tune(true); - } - } else { - mInputIdUnderSetup = null; - } - if (!mIsSetupActivityCalledByPopup) { - mOverlayManager.getSideFragmentManager().showSidePanel(false); - } + break; + case REQUEST_CODE_NOW_PLAYING: + // nothing needs to be done. onResume will restore everything. + break; + default: + // do nothing } if (data != null) { String errorMessage = data.getStringExtra(LauncherActivity.ERROR_MESSAGE); @@ -1325,16 +1421,15 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event); } - /** - * Notifies the key input focus is changed to the TV view. - */ + /** Notifies the key input focus is changed to the TV view. */ public void updateKeyInputFocus() { - mHandler.post(new Runnable() { - @Override - public void run() { - mTvView.setBlockScreenType(getDesiredBlockScreenType()); - } - }); + mHandler.post( + new Runnable() { + @Override + public void run() { + mTvView.setBlockScreenType(getDesiredBlockScreenType()); + } + }); } // It should be called before onResume. @@ -1360,12 +1455,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) { - runAfterAttachedToWindow(new Runnable() { - @Override - public void run() { - mOverlayManager.showSetupFragment(); - } - }); + runAfterAttachedToWindow( + new Runnable() { + @Override + public void run() { + mOverlayManager.showSetupFragment(); + } + }); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); if (Utils.isProgramsUri(uri)) { @@ -1388,18 +1484,34 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if ((!Utils.isChannelUriForOneChannel(mInitChannelUri) && !Utils.isChannelUriForInput(mInitChannelUri))) { - Log.w(TAG, "Malformed channel uri " + mInitChannelUri - + " tuning to default instead"); + Log.w( + TAG, + "Malformed channel uri " + mInitChannelUri + " tuning to default instead"); mInitChannelUri = null; return true; } mTuneParams = intent.getExtras(); + String programUriString = intent.getStringExtra(SearchManager.EXTRA_DATA_KEY); + Uri programUriFromIntent = + programUriString == null ? null : Uri.parse(programUriString); + long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri); + if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) { + new AsyncQueryProgramTask( + TvSingletons.getSingletons(this).getDbExecutor(), + getContentResolver(), + programUriFromIntent, + Program.PROJECTION, + null, + null, + null, + channelIdFromIntent) + .executeOnDbThread(); + } if (mTuneParams == null) { mTuneParams = new Bundle(); } if (Utils.isChannelUriForTunerInput(mInitChannelUri)) { - long channelId = ContentUris.parseId(mInitChannelUri); - mTuneParams.putLong(KEY_INIT_CHANNEL_ID, channelId); + mTuneParams.putLong(KEY_INIT_CHANNEL_ID, channelIdFromIntent); } else if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) { // If mInitChannelUri is for a passthrough TV input. String inputId = mInitChannelUri.getPathSegments().get(1); @@ -1419,16 +1531,20 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP String inputId = mInitChannelUri.getQueryParameter("input"); long channelId = Utils.getLastWatchedChannelIdForInput(this, inputId); if (channelId == Channel.INVALID_ID) { - String[] projection = { Channels._ID }; + String[] projection = {Channels._ID}; long time = System.currentTimeMillis(); - try (Cursor cursor = getContentResolver().query(uri, projection, - null, null, null)) { + try (Cursor cursor = + getContentResolver().query(uri, projection, null, null, null)) { if (cursor != null && cursor.moveToNext()) { channelId = cursor.getLong(0); } } - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity queries DB for " - + "last channel check (" + (System.currentTimeMillis() - time) + "ms)"); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log( + "MainActivity queries DB for " + + "last channel check (" + + (System.currentTimeMillis() - time) + + "ms)"); } if (channelId == Channel.INVALID_ID) { // Couldn't find any channel probably because the input hasn't been set up. @@ -1444,6 +1560,63 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return true; } + private class AsyncQueryProgramTask extends AsyncDbTask.AsyncQueryTask<Program> { + private final long mChannelIdFromIntent; + + public AsyncQueryProgramTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy, + long channelId) { + super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); + mChannelIdFromIntent = channelId; + } + + @Override + protected Program onQuery(Cursor c) { + Program program = null; + if (c != null && c.moveToNext()) { + program = Program.fromCursor(c); + } + return program; + } + + @Override + protected void onPostExecute(Program program) { + if (program == null || program.getStartTimeUtcMillis() <= System.currentTimeMillis()) { + // null or current program + return; + } + Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent); + if (channel != null) { + ScheduledRecording scheduledRecording = + TvSingletons.getSingletons(MainActivity.this) + .getDvrDataManager() + .getScheduledRecordingForProgramId(program.getId()); + DvrUiHelper.checkStorageStatusAndShowErrorMessage( + MainActivity.this, + channel.getInputId(), + new Runnable() { + @Override + public void run() { + if (CommonFeatures.DVR.isEnabled(MainActivity.this) + && scheduledRecording == null + && mDvrManager.isProgramRecordable(program)) { + DvrUiHelper.requestRecordingFutureProgram( + MainActivity.this, program, false); + } else { + DvrUiHelper.showProgramInfoDialog(MainActivity.this, program); + } + } + }); + } + } + } + private void stopTv() { stopTv(null, false); } @@ -1462,7 +1635,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mAudioManagerHelper.abandonAudioFocus(); mMediaSessionWrapper.setPlaybackState(false); } - TvApplication.getSingletons(this).getMainActivityWrapper() + TvSingletons.getSingletons(this) + .getMainActivityWrapper() .notifyCurrentChannelChange(this, null); mChannelTuner.resetCurrentChannel(); mTunePending = false; @@ -1473,9 +1647,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS); } - /** - * Says {@code text} when accessibility is turned on. - */ + /** Says {@code text} when accessibility is turned on. */ private void sendAccessibilityText(String text) { if (mAccessibilityManager.isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(); @@ -1510,8 +1682,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP finish(); return; } - SetupUtils setupUtils = SetupUtils.getInstance(this); - if (setupUtils.isFirstTune()) { + + if (mSetupUtils.isFirstTune()) { if (!mChannelTuner.areAllChannelsLoaded()) { // tune() will be called, once all channels are loaded. stopTv("tune()", false); @@ -1532,29 +1704,33 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; } if (mTvInputManagerHelper.getTunerTvInputSize() == 1) { - mOverlayManager.getSideFragmentManager().show( - new CustomizeChannelListFragment()); + mOverlayManager + .getSideFragmentManager() + .show(new CustomizeChannelListFragment()); } else { mOverlayManager.showSetupFragment(); } return; } - if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment - && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) { + if (!CommonUtils.isRunningInTest() + && mShowNewSourcesFragment + && mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) { // Show new channel sources fragment. - runAfterAttachedToWindow(new Runnable() { - @Override - public void run() { - mOverlayManager.runAfterOverlaysAreClosed(new Runnable() { + runAfterAttachedToWindow( + new Runnable() { @Override public void run() { - mOverlayManager.showNewSourcesFragment(); + mOverlayManager.runAfterOverlaysAreClosed( + new Runnable() { + @Override + public void run() { + mOverlayManager.showNewSourcesFragment(); + } + }); } }); - } - }); } - setupUtils.onTuned(); + mSetupUtils.onTuned(); if (mTuneParams != null) { Long initChannelId = mTuneParams.getLong(KEY_INIT_CHANNEL_ID); if (initChannelId == channel.getId()) { @@ -1571,9 +1747,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } // For every tune, we need to inform the tuned channel or input to a user, // if Talkback is turned on. - sendAccessibilityText(!mChannelTuner.isCurrentChannelPassthrough() ? - Utils.loadLabel(this, mTvInputManagerHelper.getTvInputInfo(channel.getInputId())) - : channel.getDisplayText()); + sendAccessibilityText( + mChannelTuner.isCurrentChannelPassthrough() + ? Utils.loadLabel( + this, mTvInputManagerHelper.getTvInputInfo(channel.getInputId())) + : channel.getDisplayText()); boolean success = mTvView.tuneTo(channel, mTuneParams, mOnTuneListener); mOnTuneListener.onTune(channel, isUnderShrunkenTvView()); @@ -1592,7 +1770,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP addToRecentChannels(channel.getId()); } Utils.setLastWatchedChannel(this, channel); - TvApplication.getSingletons(this).getMainActivityWrapper() + TvSingletons.getSingletons(this) + .getMainActivityWrapper() .notifyCurrentChannelChange(this, channel); } // We have to provide channel here instead of using TvView's channel, because TvView's @@ -1619,34 +1798,42 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // If the activity is paused shortly, runnable may not be called because all the fragments // should be closed when the activity is paused. private void runAfterAttachedToWindow(final Runnable runnable) { - final Runnable runOnlyIfActivityIsResumed = new Runnable() { - @Override - public void run() { - if (mActivityResumed) { - runnable.run(); - } - } - }; + final Runnable runOnlyIfActivityIsResumed = + new Runnable() { + @Override + public void run() { + if (mActivityResumed) { + runnable.run(); + } + } + }; if (mContentView.isAttachedToWindow()) { mHandler.post(runOnlyIfActivityIsResumed); } else { - mContentView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mContentView.getViewTreeObserver().removeOnWindowAttachListener(this); - mHandler.post(runOnlyIfActivityIsResumed); - } + mContentView + .getViewTreeObserver() + .addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mContentView + .getViewTreeObserver() + .removeOnWindowAttachListener(this); + mHandler.post(runOnlyIfActivityIsResumed); + } - @Override - public void onWindowDetached() { } - }); + @Override + public void onWindowDetached() {} + }); } } boolean isNowPlayingProgram(Channel channel, Program program) { - return program == null ? (channel != null && getCurrentProgram() == null - && channel.equals(getCurrentChannel())) : program.equals(getCurrentProgram()); + return program == null + ? (channel != null + && getCurrentProgram() == null + && channel.equals(getCurrentChannel())) + : program.equals(getCurrentProgram()); } private void addToRecentChannels(long channelId) { @@ -1659,9 +1846,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mOverlayManager.getMenu().onRecentChannelsChanged(); } - /** - * Returns the recently tuned channels. - */ + /** Returns the recently tuned channels. */ public ArrayDeque<Long> getRecentChannels() { return mRecentChannels; } @@ -1694,9 +1879,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } } - /** - * Hide the overlays when tuning to a channel from the menu (e.g. Channels). - */ + /** Hide the overlays when tuning to a channel from the menu (e.g. Channels). */ public void hideOverlaysForTune() { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE); } @@ -1712,8 +1895,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP setPreferredRefreshRate(mDefaultRefreshRate); mIsFilmModeSet = false; } else if (!mIsFilmModeSet && is24Fps) { - DisplayManager displayManager = (DisplayManager) getSystemService( - Context.DISPLAY_SERVICE); + DisplayManager displayManager = + (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); float[] refreshRates = display.getSupportedRefreshRates(); @@ -1745,8 +1928,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP String id = TvSettings.getMultiAudioId(this); String language = TvSettings.getMultiAudioLanguage(this); int channelCount = TvSettings.getMultiAudioChannelCount(this); - TvTrackInfo bestTrack = TvTrackInfoUtils - .getBestTrackInfo(tracks, id, language, channelCount); + TvTrackInfo bestTrack = + TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount); if (bestTrack != null) { String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO); if (!bestTrack.getId().equals(selectedTrack)) { @@ -1787,8 +1970,13 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP mTvOptionsManager.onClosedCaptionsChanged(track, i); } if (DEBUG) { - Log.d(TAG, "Subtitle Track Selected {id=" + track.getId() - + ", language=" + track.getLanguage() + "}"); + Log.d( + TAG, + "Subtitle Track Selected {id=" + + track.getId() + + ", language=" + + track.getLanguage() + + "}"); } return; } else if (alternativeTrack == null) { @@ -1801,12 +1989,17 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (!alternativeTrack.getId().equals(selectedTrackId)) { selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack, alternativeTrackIndex); } else { - mTvOptionsManager - .onClosedCaptionsChanged(alternativeTrack, alternativeTrackIndex); + mTvOptionsManager.onClosedCaptionsChanged( + alternativeTrack, alternativeTrackIndex); } if (DEBUG) { - Log.d(TAG, "Subtitle Track Selected {id=" + alternativeTrack.getId() - + ", language=" + alternativeTrack.getLanguage() + "}"); + Log.d( + TAG, + "Subtitle Track Selected {id=" + + alternativeTrack.getId() + + ", language=" + + alternativeTrack.getLanguage() + + "}"); } return; } @@ -1820,8 +2013,11 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } public void showProgramGuideSearchFragment() { - getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment) - .addToBackStack(null).commit(); + getFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, mSearchFragment) + .addToBackStack(null) + .commit(); } @Override @@ -1853,6 +2049,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } } if (mOverlayManager != null) { + mAccessibilityManager.removeAccessibilityStateChangeListener(mOverlayManager); mOverlayManager.release(); } mMemoryManageables.clear(); @@ -1874,7 +2071,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if (mTvInputManagerHelper != null) { mTvInputManagerHelper.clearTvInputLabels(); - if (Features.TUNER.isEnabled(this)) { + if (TvFeatures.TUNER.isEnabled(this)) { mTvInputManagerHelper.removeCallback(mTvInputCallback); } } @@ -1895,7 +2092,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return false; case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH: default: - // pass through + // fall through } if (mSearchFragment.isVisible()) { return super.onKeyDown(keyCode, event); @@ -1903,35 +2100,51 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (!mChannelTuner.areAllChannelsLoaded()) { return false; } + if (handleUpDownKeys(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) { if (!mChannelTuner.isCurrentChannelPassthrough()) { switch (keyCode) { case KeyEvent.KEYCODE_CHANNEL_UP: case KeyEvent.KEYCODE_DPAD_UP: - if (event.getRepeatCount() == 0 + if ((event == null || event.getRepeatCount() == 0) && mChannelTuner.getBrowsableChannelCount() > 0) { // message sending should be done before moving channel, because we use the // existence of message to decide if users are switching channel. - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, - System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + if (event != null) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()), + CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + } moveToAdjacentChannel(true, false); mTracker.sendChannelUp(); } return true; case KeyEvent.KEYCODE_CHANNEL_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN: - if (event.getRepeatCount() == 0 + if ((event == null || event.getRepeatCount() == 0) && mChannelTuner.getBrowsableChannelCount() > 0) { // message sending should be done before moving channel, because we use the // existence of message to decide if users are switching channel. - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, - System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + if (event != null) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage( + MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()), + CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + } moveToAdjacentChannel(false, false); mTracker.sendChannelDown(); } return true; + default: // fall out } } - return super.onKeyDown(keyCode, event); + return false; } @Override @@ -1969,7 +2182,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return false; case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH: default: - // pass through + // fall through } if (mSearchFragment.isVisible()) { if (keyCode == KeyEvent.KEYCODE_BACK) { @@ -1999,6 +2212,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP case KeyEvent.KEYCODE_MENU: showSettingsFragment(); return true; + default: // fall out } } else { if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) { @@ -2009,9 +2223,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP case KeyEvent.KEYCODE_DPAD_RIGHT: if (!mTvView.isVideoOrAudioAvailable() && mTvView.getVideoUnavailableReason() - == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) { - DvrUiHelper.startSchedulesActivityForTuneConflict(this, - mChannelTuner.getCurrentChannel()); + == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) { + DvrUiHelper.startSchedulesActivityForTuneConflict( + this, mChannelTuner.getCurrentChannel()); return true; } if (!PermissionUtils.hasModifyParentalControls(this)) { @@ -2019,16 +2233,18 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } PinDialogFragment dialog = null; if (mTvView.isScreenBlocked()) { - dialog = PinDialogFragment - .create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL); + dialog = + PinDialogFragment.create( + PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL); } else if (mTvView.isContentBlocked()) { - dialog = PinDialogFragment - .create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + dialog = + PinDialogFragment.create( + PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, mTvView.getBlockedContentRating().flattenToString()); } if (dialog != null) { - mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, - false); + mOverlayManager.showDialogFragment( + PinDialogFragment.DIALOG_TAG, dialog, false); } return true; case KeyEvent.KEYCODE_WINDOW: @@ -2064,49 +2280,56 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (!SystemProperties.USE_DEBUG_KEYS.getValue()) { break; } - // Pass through. - case KeyEvent.KEYCODE_CAPTIONS: { + // fall through. + case KeyEvent.KEYCODE_CAPTIONS: mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment()); return true; - } case KeyEvent.KEYCODE_A: if (!SystemProperties.USE_DEBUG_KEYS.getValue()) { break; } - // Pass through. - case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + // fall through. + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment()); return true; - } - case KeyEvent.KEYCODE_INFO: { + case KeyEvent.KEYCODE_INFO: mOverlayManager.showBanner(); return true; - } case KeyEvent.KEYCODE_MEDIA_RECORD: - case KeyEvent.KEYCODE_V: { + case KeyEvent.KEYCODE_V: Channel currentChannel = getCurrentChannel(); if (currentChannel != null && mDvrManager != null) { boolean isRecording = mDvrManager.getCurrentRecording(currentChannel.getId()) != null; if (!isRecording) { if (!mDvrManager.isChannelRecordable(currentChannel)) { - Toast.makeText(this, R.string.dvr_msg_cannot_record_program, - Toast.LENGTH_SHORT).show(); + Toast.makeText( + this, + R.string.dvr_msg_cannot_record_program, + Toast.LENGTH_SHORT) + .show(); } else { - Program program = mProgramDataManager - .getCurrentProgram(currentChannel.getId()); - DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, - currentChannel.getInputId(), new Runnable() { + Program program = + mProgramDataManager.getCurrentProgram( + currentChannel.getId()); + DvrUiHelper.checkStorageStatusAndShowErrorMessage( + this, + currentChannel.getInputId(), + new Runnable() { @Override public void run() { DvrUiHelper.requestRecordingCurrentProgram( MainActivity.this, - currentChannel, program, false); + currentChannel, + program, + false); } }); } } else { - DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(), + DvrUiHelper.showStopRecordingDialog( + this, + currentChannel.getId(), DvrStopRecordingFragment.REASON_USER_STOP, new HalfSizedDialogFragment.OnActionClickListener() { @Override @@ -2124,7 +2347,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } } return true; - } + default: // fall out } } if (keyCode == KeyEvent.KEYCODE_WINDOW) { @@ -2162,6 +2385,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP case KeyEvent.KEYCODE_D: mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment()); return true; + default: // fall out } } return super.onKeyUp(keyCode, event); @@ -2196,13 +2420,19 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // We need to hide overlay first, before moving the activity to PIP. If not, UI will // be shown during PIP stack resizing, because UI and its animation is stuck during // PIP resizing. - mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); - mHandler.post(new Runnable() { - @Override - public void run() { - MainActivity.super.enterPictureInPictureMode(); - } - }); + mIsInPIPMode = true; + if (mOverlayManager.isOverlayOpened()) { + mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); + mHandler.post( + new Runnable() { + @Override + public void run() { + MainActivity.super.enterPictureInPictureMode(); + } + }); + } else { + MainActivity.super.enterPictureInPictureMode(); + } } @Override @@ -2248,7 +2478,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } if (isKeyEventBlocked()) { if ((event.getKeyCode() == KeyEvent.KEYCODE_BACK - || event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B) && mNeedShowBackKeyGuide) { + || event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B) + && mNeedShowBackKeyGuide) { // KeyEvent.KEYCODE_BUTTON_B is also used like the back button. Toast.makeText(this, R.string.msg_back_key_guide, Toast.LENGTH_SHORT).show(); mNeedShowBackKeyGuide = false; @@ -2283,7 +2514,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } else if (channel.equals(mTvView.getCurrentChannel())) { mOverlayManager.updateChannelBannerAndShowIfNeeded( TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE); - } else if (channel == mChannelTuner.getCurrentChannel()) { + } else if (channel.equals(mChannelTuner.getCurrentChannel())) { // Channel banner is already updated in moveToAdjacentChannel tune(false); } else if (mChannelTuner.moveToChannel(channel)) { @@ -2296,24 +2527,23 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } /** - * This method just moves the channel in the channel map and updates the channel banner, - * but doesn't actually tune to the channel. - * The caller of this method should call {@link #tune} in the end. + * This method just moves the channel in the channel map and updates the channel banner, but + * doesn't actually tune to the channel. The caller of this method should call {@link #tune} in + * the end. * * @param channelUp {@code true} for channel up, and {@code false} for channel down. * @param fastTuning {@code true} if fast tuning is requested. */ private void moveToAdjacentChannel(boolean channelUp, boolean fastTuning) { if (mChannelTuner.moveToAdjacentBrowsableChannel(channelUp)) { - mOverlayManager.updateChannelBannerAndShowIfNeeded(fastTuning ? - TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST - : TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE); + mOverlayManager.updateChannelBannerAndShowIfNeeded( + fastTuning + ? TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST + : TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE); } } - /** - * Set the main TV view which holds HDMI-CEC active source based on the sound mode - */ + /** Set the main TV view which holds HDMI-CEC active source based on the sound mode */ private void restoreMainTvView() { mTvView.setMain(); } @@ -2355,8 +2585,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private void selectTrack(int type, TvTrackInfo track, int trackIndex) { mTvView.selectTrack(type, track == null ? null : track.getId()); if (type == TvTrackInfo.TYPE_AUDIO) { - mTvOptionsManager.onMultiAudioChanged(track == null ? null : - Utils.getMultiAudioString(this, track, false)); + mTvOptionsManager.onMultiAudioChanged( + track == null ? null : Utils.getMultiAudioString(this, track, false)); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex); } @@ -2414,7 +2644,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP private void updateAvailabilityToast() { if (mTvView.isVideoAvailable() - || mTvView.getCurrentChannel() != mChannelTuner.getCurrentChannel()) { + || !Objects.equals( + mTvView.getCurrentChannel(), mChannelTuner.getCurrentChannel())) { return; } @@ -2428,50 +2659,38 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: - Toast.makeText(this, R.string.msg_channel_unavailable_unknown, - Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT) + .show(); break; } } - /** - * Returns {@code true} if some overlay UI will be shown when the activity is resumed. - */ + /** Returns {@code true} if some overlay UI will be shown when the activity is resumed. */ public boolean willShowOverlayUiWhenResume() { return mInputToSetUp != null || mShowProgramGuide || mShowSelectInputView; } - /** - * Returns the current parental control settings. - */ + /** Returns the current parental control settings. */ public ParentalControlSettings getParentalControlSettings() { return mTvInputManagerHelper.getParentalControlSettings(); } - /** - * Returns a ContentRatingsManager instance. - */ + /** Returns a ContentRatingsManager instance. */ public ContentRatingsManager getContentRatingsManager() { return mTvInputManagerHelper.getContentRatingsManager(); } - /** - * Returns the current captioning settings. - */ + /** Returns the current captioning settings. */ public CaptionSettings getCaptionSettings() { return mCaptionSettings; } - /** - * Adds the {@link OnActionClickListener}. - */ + /** Adds the {@link OnActionClickListener}. */ public void addOnActionClickListener(OnActionClickListener listener) { mOnActionClickListeners.add(listener); } - /** - * Removes the {@link OnActionClickListener}. - */ + /** Removes the {@link OnActionClickListener}. */ public void removeOnActionClickListener(OnActionClickListener listener) { mOnActionClickListeners.remove(listener); } @@ -2490,7 +2709,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP // Initialize TV app for test. The setup process should be finished before the Live TV app is // started. We only enable all the channels here. private void initForTest() { - if (!TvCommonUtils.isRunningInTest()) { + if (!CommonUtils.isRunningInTest()) { return; } @@ -2505,16 +2724,18 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP } mLazyInitialized = true; // Running initialization. - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (mActivityStarted) { - initAnimations(); - initSideFragments(); - initMenuItemViews(); - } - } - }, LAZY_INITIALIZATION_DELAY); + mHandler.postDelayed( + new Runnable() { + @Override + public void run() { + if (mActivityStarted) { + initAnimations(); + initSideFragments(); + initMenuItemViews(); + } + } + }, + LAZY_INITIALIZATION_DELAY); } private void initAnimations() { @@ -2560,6 +2781,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); mainActivity.moveToAdjacentChannel(true, true); break; + default: // fall out } } @@ -2594,17 +2816,19 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP if (mTvView.isFadedOut()) { mTvView.removeFadeEffect(); } - Toast.makeText(MainActivity.this, R.string.msg_channel_unavailable_unknown, - Toast.LENGTH_SHORT).show(); + Toast.makeText( + MainActivity.this, + R.string.msg_channel_unavailable_unknown, + Toast.LENGTH_SHORT) + .show(); } @Override public void onStreamInfoChanged(StreamInfo info) { if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) { - mTracker.sendChannelTuneTime(info.getCurrentChannel(), - mTuneDurationTimer.reset()); + mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset()); } - if (info.isVideoOrAudioAvailable() && mChannel == getCurrentChannel()) { + if (info.isVideoOrAudioAvailable() && mChannel.equals(getCurrentChannel())) { mOverlayManager.updateChannelBannerAndShowIfNeeded( TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO); } @@ -2629,10 +2853,12 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP return; } Channel currentChannel = - mChannelDataManager.getChannel(ContentUris.parseId(channel)); + mChannelDataManager.getChannel(ContentUriUtils.safeParseId(channel)); if (currentChannel == null) { - Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI " - + channel); + Log.e( + TAG, + "onChannelRetuned is called but can't find a channel with the URI " + + channel); return; } if (isChannelChangeKeyDownReceived()) { @@ -2647,15 +2873,16 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP @Override public void onContentBlocked() { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "MainActivity.MyOnTuneListener.onContentBlocked removes timer"); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log("MainActivity.MyOnTuneListener.onContentBlocked removes timer"); Debug.removeTimer(Debug.TAG_START_UP_TIMER); mTuneDurationTimer.reset(); TvContentRating rating = mTvView.getBlockedContentRating(); // When tuneTo was called while TV view was shrunken, if the channel id is the same // with the channel watched before shrunken, we allow the rating which was allowed // before. - if (mWasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken + if (mWasUnderShrunkenTvView + && mUnlockAllowedRatingBeforeShrunken && mChannelBeforeShrunkenTvView.equals(mChannel) && rating.equals(mAllowedRatingBeforeShrunken)) { mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); diff --git a/src/com/android/tv/MainActivityWrapper.java b/src/com/android/tv/MainActivityWrapper.java index 5af5079f..6cecb436 100644 --- a/src/com/android/tv/MainActivityWrapper.java +++ b/src/com/android/tv/MainActivityWrapper.java @@ -20,14 +20,12 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.ArraySet; - -import com.android.tv.data.Channel; - +import com.android.tv.data.api.Channel; import java.util.Set; /** - * A wrapper for safely getting the current {@link MainActivity}. - * Note that this class is not thread-safe. All the public methods should be called on main thread. + * A wrapper for safely getting the current {@link MainActivity}. Note that this class is not + * thread-safe. All the public methods should be called on main thread. */ @MainThread public final class MainActivityWrapper { @@ -36,39 +34,31 @@ public final class MainActivityWrapper { private final Set<OnCurrentChannelChangeListener> mListeners = new ArraySet<>(); /** - * Returns the current main activity. - * <b>WARNING</b> do not keep a reference to MainActivity, leaking activities is expensive. + * Returns the current main activity. <b>WARNING</b> do not keep a reference to MainActivity, + * leaking activities is expensive. */ MainActivity getMainActivity() { return mActivity; } - /** - * Checks if the given {@code activity} is the current main activity. - */ + /** Checks if the given {@code activity} is the current main activity. */ boolean isCurrent(MainActivity activity) { return activity != null && mActivity == activity; } - /** - * Sets the currently created main activity instance. - */ + /** Sets the currently created main activity instance. */ public void onMainActivityCreated(@NonNull MainActivity activity) { mActivity = activity; } - /** - * Unsets the main activity instance. - */ + /** Unsets the main activity instance. */ public void onMainActivityDestroyed(@NonNull MainActivity activity) { if (mActivity == activity) { mActivity = null; } } - /** - * Notifies the current channel change. - */ + /** Notifies the current channel change. */ void notifyCurrentChannelChange(@NonNull MainActivity caller, @Nullable Channel channel) { if (mActivity == caller) { for (OnCurrentChannelChangeListener listener : mListeners) { @@ -77,48 +67,34 @@ public final class MainActivityWrapper { } } - /** - * Checks if the main activity is created. - */ + /** Checks if the main activity is created. */ public boolean isCreated() { return mActivity != null; } - /** - * Checks if the main activity is started. - */ + /** Checks if the main activity is started. */ public boolean isStarted() { return mActivity != null && mActivity.isActivityStarted(); } - /** - * Checks if the main activity is resumed. - */ + /** Checks if the main activity is resumed. */ public boolean isResumed() { return mActivity != null && mActivity.isActivityResumed(); } - /** - * Adds OnCurrentChannelChangeListener. - */ + /** Adds OnCurrentChannelChangeListener. */ public void addOnCurrentChannelChangeListener(OnCurrentChannelChangeListener listener) { mListeners.add(listener); } - /** - * Removes OnCurrentChannelChangeListener. - */ + /** Removes OnCurrentChannelChangeListener. */ public void removeOnCurrentChannelChangeListener(OnCurrentChannelChangeListener listener) { mListeners.remove(listener); } - /** - * Listener for the current channel change in main activity. - */ + /** Listener for the current channel change in main activity. */ public interface OnCurrentChannelChangeListener { - /** - * Called when the current channel changes. - */ + /** Called when the current channel changes. */ void onCurrentChannelChange(@Nullable Channel channel); } } diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java index da6ad2a4..43cd74dd 100644 --- a/src/com/android/tv/MediaSessionWrapper.java +++ b/src/com/android/tv/MediaSessionWrapper.java @@ -16,6 +16,7 @@ package com.android.tv; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -28,12 +29,12 @@ import android.media.tv.TvInputInfo; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; - -import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.util.ImageLoader; +import com.android.tv.data.api.Channel; import com.android.tv.util.Utils; +import com.android.tv.util.images.ImageLoader; /** * A wrapper class for {@link MediaSession} to support common operations on media sessions for @@ -41,34 +42,47 @@ import com.android.tv.util.Utils; */ class MediaSessionWrapper { private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession"; - private static PlaybackState MEDIA_SESSION_STATE_PLAYING = new PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f) - .build(); - private static PlaybackState MEDIA_SESSION_STATE_STOPPED = new PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f) - .build(); + + private static final PlaybackState MEDIA_SESSION_STATE_PLAYING = + new PlaybackState.Builder() + .setState( + PlaybackState.STATE_PLAYING, + PlaybackState.PLAYBACK_POSITION_UNKNOWN, + 1.0f) + .build(); + + private static final PlaybackState MEDIA_SESSION_STATE_STOPPED = + new PlaybackState.Builder() + .setState( + PlaybackState.STATE_STOPPED, + PlaybackState.PLAYBACK_POSITION_UNKNOWN, + 0.0f) + .build(); private final Context mContext; private final MediaSession mMediaSession; private int mNowPlayingCardWidth; private int mNowPlayingCardHeight; - MediaSessionWrapper(Context context) { + MediaSessionWrapper(Context context, PendingIntent pendingIntent) { mContext = context; mMediaSession = new MediaSession(context, MEDIA_SESSION_TAG); - mMediaSession.setCallback(new MediaSession.Callback() { - @Override - public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { - // Consume the media button event here. Should not send it to other apps. - return true; - } - }); - mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | - MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); - mNowPlayingCardWidth = mContext.getResources().getDimensionPixelSize( - R.dimen.notif_card_img_max_width); - mNowPlayingCardHeight = mContext.getResources().getDimensionPixelSize( - R.dimen.notif_card_img_height); + mMediaSession.setCallback( + new MediaSession.Callback() { + @Override + public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { + // Consume the media button event here. Should not send it to other apps. + return true; + } + }); + mMediaSession.setFlags( + MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mMediaSession.setSessionActivity(pendingIntent); + mNowPlayingCardWidth = + mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = + mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); } /** @@ -90,8 +104,8 @@ class MediaSessionWrapper { /** * Updates media session according to the current TV playback status. * - * @param blocked {@code true} if the current channel is blocked, either by user settings or - * the current program's content ratings. + * @param blocked {@code true} if the current channel is blocked, either by user settings or the + * current program's content ratings. * @param currentChannel The currently playing channel. * @param currentProgram The currently playing program. */ @@ -103,10 +117,12 @@ class MediaSessionWrapper { // If the channel is blocked, display a lock and a short text on the Now Playing Card if (blocked) { - Bitmap art = BitmapFactory.decodeResource(mContext.getResources(), - R.drawable.ic_message_lock_preview); - updateMediaMetadata(mContext.getResources() - .getString(R.string.channel_banner_locked_channel_title), art); + Bitmap art = + BitmapFactory.decodeResource( + mContext.getResources(), R.drawable.ic_message_lock_preview); + updateMediaMetadata( + mContext.getResources().getString(R.string.channel_banner_locked_channel_title), + art); setPlaybackState(true); return; } @@ -139,22 +155,32 @@ class MediaSessionWrapper { private String getChannelName(Channel channel) { if (channel.isPassthrough()) { - TvInputInfo input = TvApplication.getSingletons(mContext).getTvInputManagerHelper() - .getTvInputInfo(channel.getInputId()); + TvInputInfo input = + TvSingletons.getSingletons(mContext) + .getTvInputManagerHelper() + .getTvInputInfo(channel.getInputId()); return Utils.loadLabel(mContext, input); } else { return channel.getDisplayName(); } } - private void updatePosterArt(Channel currentChannel, Program currentProgram, - String cardTitleText, @Nullable Bitmap posterArt, @Nullable String posterArtUri) { + private void updatePosterArt( + Channel currentChannel, + Program currentProgram, + String cardTitleText, + @Nullable Bitmap posterArt, + @Nullable String posterArtUri) { if (posterArt != null) { updateMediaMetadata(cardTitleText, posterArt); } else if (posterArtUri != null) { - ImageLoader.loadBitmap(mContext, posterArtUri, mNowPlayingCardWidth, - mNowPlayingCardHeight, new ProgramPosterArtCallback(this, currentChannel, - currentProgram, cardTitleText)); + ImageLoader.loadBitmap( + mContext, + posterArtUri, + mNowPlayingCardWidth, + mNowPlayingCardHeight, + new ProgramPosterArtCallback( + this, currentChannel, currentProgram, cardTitleText)); } else { updateMediaMetadata(cardTitleText, R.drawable.default_now_card); } @@ -176,7 +202,7 @@ class MediaSessionWrapper { } private void updateMediaMetadata(final String title, final int imageResId) { - new AsyncTask<Void, Void, Void> () { + new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... arg0) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); @@ -192,14 +218,22 @@ class MediaSessionWrapper { }.execute(); } - private static class ProgramPosterArtCallback extends - ImageLoader.ImageLoaderCallback<MediaSessionWrapper> { + @VisibleForTesting + MediaSession getMediaSession() { + return mMediaSession; + } + + private static class ProgramPosterArtCallback + extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> { private final Channel mChannel; private final Program mProgram; private final String mCardTitleText; - ProgramPosterArtCallback(MediaSessionWrapper sessionWrapper, Channel channel, - Program program, String cardTitleText) { + ProgramPosterArtCallback( + MediaSessionWrapper sessionWrapper, + Channel channel, + Program program, + String cardTitleText) { super(sessionWrapper); mChannel = channel; mProgram = program; diff --git a/src/com/android/tv/SelectInputActivity.java b/src/com/android/tv/SelectInputActivity.java index c68a1ad0..56747044 100644 --- a/src/com/android/tv/SelectInputActivity.java +++ b/src/com/android/tv/SelectInputActivity.java @@ -23,15 +23,12 @@ import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.Bundle; import android.view.KeyEvent; - -import com.android.tv.data.Channel; +import com.android.tv.data.ChannelImpl; import com.android.tv.ui.SelectInputView; import com.android.tv.ui.SelectInputView.OnInputSelectedCallback; import com.android.tv.util.Utils; -/** - * An activity to select input. - */ +/** An activity to select input. */ public class SelectInputActivity extends Activity { private SelectInputView mSelectInputView; @@ -41,30 +38,37 @@ public class SelectInputActivity extends Activity { ((TvApplication) getApplicationContext()).setSelectInputActivity(this); setContentView(R.layout.activity_select_input); mSelectInputView = (SelectInputView) findViewById(R.id.scene_transition_common); - mSelectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() { - @Override - public void onTunerInputSelected() { - startTvWithChannel(TvContract.Channels.CONTENT_URI); - } + mSelectInputView.setOnInputSelectedCallback( + new OnInputSelectedCallback() { + @Override + public void onTunerInputSelected() { + startTvWithChannel(TvContract.Channels.CONTENT_URI); + } - @Override - public void onPassthroughInputSelected(TvInputInfo input) { - startTvWithChannel(TvContract.buildChannelUriForPassthroughInput(input.getId())); - } + @Override + public void onPassthroughInputSelected(TvInputInfo input) { + startTvWithChannel( + TvContract.buildChannelUriForPassthroughInput(input.getId())); + } - private void startTvWithChannel(Uri channelUri) { - Intent intent = new Intent(Intent.ACTION_VIEW, channelUri, - SelectInputActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finish(); - } - }); + private void startTvWithChannel(Uri channelUri) { + Intent intent = + new Intent( + Intent.ACTION_VIEW, + channelUri, + SelectInputActivity.this, + MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + }); String channelUriString = Utils.getLastWatchedChannelUri(this); if (channelUriString != null) { Uri channelUri = Uri.parse(channelUriString); if (TvContract.isChannelUriForPassthroughInput(channelUri)) { - mSelectInputView.setCurrentChannel(Channel.createPassthroughChannel(channelUri)); + mSelectInputView.setCurrentChannel( + ChannelImpl.createPassthroughChannel(channelUri)); } // No need to set the tuner channel because it's the default selection. } diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index f0f54413..199ea51d 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -26,23 +26,23 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.MainThread; import android.util.Log; - import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonConstants; +import com.android.tv.common.actions.InputSetupActionUtils; +import com.android.tv.common.experiments.Experiments; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.experiments.Experiments; +import com.android.tv.data.epg.EpgInputWhiteList; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - +import com.google.android.tv.partner.support.EpgContract; import java.util.concurrent.TimeUnit; /** * An activity to launch a TV input setup activity. * - * <p> After setup activity is finished, all channels will be browsable. + * <p>After setup activity is finished, all channels will be browsable. */ public class SetupPassthroughActivity extends Activity { private static final String TAG = "SetupPassthroughAct"; @@ -55,35 +55,45 @@ public class SetupPassthroughActivity extends Activity { private TvInputInfo mTvInputInfo; private Intent mActivityAfterCompletion; private boolean mEpgFetcherDuringScan; + private EpgInputWhiteList mEpgInputWhiteList; @Override public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - ApplicationSingletons appSingletons = TvApplication.getSingletons(this); - TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper(); + TvSingletons tvSingletons = TvSingletons.getSingletons(this); + TvInputManagerHelper inputManager = tvSingletons.getTvInputManagerHelper(); Intent intent = getIntent(); - String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); + String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID); mTvInputInfo = inputManager.getTvInputInfo(inputId); - mActivityAfterCompletion = intent.getParcelableExtra( - TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION); - boolean needToFetchEpg = Utils.isInternalTvInput(this, mTvInputInfo.getId()) - && Experiments.CLOUD_EPG.get(); + mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig()); + mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent); + boolean needToFetchEpg = + mTvInputInfo != null + && Utils.isInternalTvInput(this, mTvInputInfo.getId()) + && Experiments.CLOUD_EPG.get(); if (needToFetchEpg) { // In case when the activity is restored, this flag should be restored as well. mEpgFetcherDuringScan = true; } if (savedInstanceState == null) { - SoftPreconditions.checkState( - intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP)); + SoftPreconditions.checkArgument( + InputSetupActionUtils.hasInputSetupAction(intent), + TAG, + "Unsupported action %s", + intent.getAction()); if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); if (mTvInputInfo == null) { Log.w(TAG, "There is no input with the ID " + inputId + "."); finish(); return; } - Intent setupIntent = - intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT); + if (intent.getExtras() == null) { + Log.w(TAG, "There is no extra info in the intent"); + finish(); + return; + } + Intent setupIntent = InputSetupActionUtils.getExtraSetupIntent(intent); if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent); if (setupIntent == null) { Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); @@ -95,7 +105,7 @@ public class SetupPassthroughActivity extends Activity { // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during // setupIntent.putExtras(intent.getExtras()). Bundle extras = intent.getExtras(); - extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT); + InputSetupActionUtils.removeSetupIntent(extras); setupIntent.putExtras(extras); try { startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); @@ -109,48 +119,54 @@ public class SetupPassthroughActivity extends Activity { sScanTimeoutMonitor = new ScanTimeoutMonitor(this); } sScanTimeoutMonitor.startMonitoring(); - EpgFetcher.getInstance(this).onChannelScanStarted(); + TvSingletons.getSingletons(this).getEpgFetcher().onChannelScanStarted(); } } } @Override public void onActivityResult(int requestCode, final int resultCode, final Intent data) { - if (DEBUG) Log.d(TAG, "onActivityResult"); + if (DEBUG) + Log.d(TAG, "onActivityResult(" + requestCode + ", " + resultCode + ", " + data + ")"); if (sScanTimeoutMonitor != null) { sScanTimeoutMonitor.stopMonitoring(); } // Note: It's not guaranteed that this method is always called after scanning. - boolean setupComplete = requestCode == REQUEST_START_SETUP_ACTIVITY - && resultCode == Activity.RESULT_OK; + boolean setupComplete = + requestCode == REQUEST_START_SETUP_ACTIVITY && resultCode == Activity.RESULT_OK; // Tells EpgFetcher that channel source setup is finished. + EpgFetcher epgFetcher = TvSingletons.getSingletons(this).getEpgFetcher(); if (mEpgFetcherDuringScan) { - EpgFetcher.getInstance(this).onChannelScanFinished(); + epgFetcher.onChannelScanFinished(); } if (!setupComplete) { setResult(resultCode, data); finish(); return; } - SetupUtils.getInstance(this).onTvInputSetupFinished(mTvInputInfo.getId(), new Runnable() { - @Override - public void run() { - if (mActivityAfterCompletion != null) { - try { - startActivity(mActivityAfterCompletion); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Activity launch failed", e); - } - } - setResult(resultCode, data); - finish(); - } - }); + TvSingletons.getSingletons(this) + .getSetupUtils() + .onTvInputSetupFinished( + mTvInputInfo.getId(), + new Runnable() { + @Override + public void run() { + if (mActivityAfterCompletion != null) { + try { + startActivity(mActivityAfterCompletion); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity launch failed", e); + } + } + setResult(resultCode, data); + finish(); + } + }); } /** - * Monitors the scan progress and notifies the timeout of the scanning. - * The purpose of this monitor is to call EpgFetcher.onChannelScanFinished() in case when + * Monitors the scan progress and notifies the timeout of the scanning. The purpose of this + * monitor is to call EpgFetcher.onChannelScanFinished() in case when * SetupPassthroughActivity.onActivityResult() is not called properly. b/36008534 */ @MainThread @@ -161,33 +177,37 @@ public class SetupPassthroughActivity extends Activity { private final Context mContext; private final ChannelDataManager mChannelDataManager; private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final Runnable mScanTimeoutRunnable = new Runnable() { - @Override - public void run() { - Log.w(TAG, "No channels has been added for a while." + - " The scan might have finished unexpectedly."); - onScanTimedOut(); - } - }; - private final Listener mChannelDataManagerListener = new Listener() { - @Override - public void onLoadFinished() { - setupTimer(); - } + private final Runnable mScanTimeoutRunnable = + new Runnable() { + @Override + public void run() { + Log.w( + TAG, + "No channels has been added for a while." + + " The scan might have finished unexpectedly."); + onScanTimedOut(); + } + }; + private final Listener mChannelDataManagerListener = + new Listener() { + @Override + public void onLoadFinished() { + setupTimer(); + } - @Override - public void onChannelListUpdated() { - setupTimer(); - } + @Override + public void onChannelListUpdated() { + setupTimer(); + } - @Override - public void onChannelBrowsableChanged() { } - }; + @Override + public void onChannelBrowsableChanged() {} + }; private boolean mStarted; private ScanTimeoutMonitor(Context context) { mContext = context.getApplicationContext(); - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + mChannelDataManager = TvSingletons.getSingletons(context).getChannelDataManager(); } private void startMonitoring() { @@ -215,7 +235,7 @@ public class SetupPassthroughActivity extends Activity { private void onScanTimedOut() { stopMonitoring(); - EpgFetcher.getInstance(mContext).onChannelScanFinished(); + TvSingletons.getSingletons(mContext).getEpgFetcher().onChannelScanFinished(); } } } diff --git a/src/com/android/tv/Starter.java b/src/com/android/tv/Starter.java new file mode 100644 index 00000000..22fda0bd --- /dev/null +++ b/src/com/android/tv/Starter.java @@ -0,0 +1,42 @@ +/* + * 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; + +import android.content.Context; +import android.util.Log; + +/** Initializes TvApplication. */ +public interface Starter { + + /** + * Initializes TvApplication. + * + * <p>Note: it should be called at the beginning of any Service.onCreate, Activity.onCreate, or + * BroadcastReceiver.onCreate. + */ + static void start(Context context) { + // TODO(b/63064354) TvApplication should not have to know if it is "the main process" + if (context.getApplicationContext() instanceof Starter) { + Starter starter = (Starter) context.getApplicationContext(); + starter.start(); + } else { + // Application context can be MockTvApplication. + Log.w("Start", "It is not a context of TvApplication"); + } + } + + void start(); +} diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index 70885936..bb3574d7 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -27,20 +27,18 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.Range; - import com.android.tv.analytics.Tracker; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.ui.TunableTvView; -import com.android.tv.ui.TunableTvView.TimeShiftListener; +import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -53,11 +51,11 @@ import java.util.Queue; import java.util.concurrent.TimeUnit; /** - * A class which manages the time shift feature in Live TV. It consists of two parts. - * {@link PlayController} controls the playback such as play/pause, rewind and fast-forward using - * {@link TunableTvView} which communicates with TvInputService through - * {@link android.media.tv.TvInputService.Session}. - * {@link ProgramManager} loads programs of the current channel in the background. + * A class which manages the time shift feature in Live TV. It consists of two parts. {@link + * PlayController} controls the playback such as play/pause, rewind and fast-forward using {@link + * TunableTvView} which communicates with TvInputService through {@link + * android.media.tv.TvInputService.Session}. {@link ProgramManager} loads programs of the current + * channel in the background. */ public class TimeShiftManager { private static final String TAG = "TimeShiftManager"; @@ -66,12 +64,14 @@ public class TimeShiftManager { @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING}) public @interface PlayStatus {} - public static final int PLAY_STATUS_PAUSED = 0; + + public static final int PLAY_STATUS_PAUSED = 0; public static final int PLAY_STATUS_PLAYING = 1; @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_SPEED_1X, PLAY_SPEED_2X, PLAY_SPEED_3X, PLAY_SPEED_4X, PLAY_SPEED_5X}) - public @interface PlaySpeed{} + public @interface PlaySpeed {} + public static final int PLAY_SPEED_1X = 1; public static final int PLAY_SPEED_2X = 2; public static final int PLAY_SPEED_3X = 3; @@ -80,15 +80,25 @@ public class TimeShiftManager { @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_DIRECTION_FORWARD, PLAY_DIRECTION_BACKWARD}) - public @interface PlayDirection{} - public static final int PLAY_DIRECTION_FORWARD = 0; + public @interface PlayDirection {} + + public static final int PLAY_DIRECTION_FORWARD = 0; public static final int PLAY_DIRECTION_BACKWARD = 1; @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {TIME_SHIFT_ACTION_ID_PLAY, TIME_SHIFT_ACTION_ID_PAUSE, - TIME_SHIFT_ACTION_ID_REWIND, TIME_SHIFT_ACTION_ID_FAST_FORWARD, - TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT}) - public @interface TimeShiftActionId{} + @IntDef( + flag = true, + value = { + TIME_SHIFT_ACTION_ID_PLAY, + TIME_SHIFT_ACTION_ID_PAUSE, + TIME_SHIFT_ACTION_ID_REWIND, + TIME_SHIFT_ACTION_ID_FAST_FORWARD, + TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, + TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT + } + ) + public @interface TimeShiftActionId {} + public static final int TIME_SHIFT_ACTION_ID_PLAY = 1; public static final int TIME_SHIFT_ACTION_ID_PAUSE = 1 << 1; public static final int TIME_SHIFT_ACTION_ID_REWIND = 1 << 2; @@ -100,8 +110,7 @@ public class TimeShiftManager { private static final int MSG_PREFETCH_PROGRAM = 1001; private static final long REQUEST_CURRENT_POSITION_INTERVAL = TimeUnit.SECONDS.toMillis(1); private static final long MAX_DUMMY_PROGRAM_DURATION = TimeUnit.MINUTES.toMillis(30); - @VisibleForTesting - static final long INVALID_TIME = -1; + @VisibleForTesting static final long INVALID_TIME = -1; static final long CURRENT_TIME = -2; private static final long PREFETCH_TIME_OFFSET_FROM_PROGRAM_END = TimeUnit.MINUTES.toMillis(1); private static final long PREFETCH_DURATION_FOR_NEXT = TimeUnit.HOURS.toMillis(2); @@ -109,57 +118,57 @@ public class TimeShiftManager { private static final long ALLOWED_START_TIME_OFFSET = TimeUnit.DAYS.toMillis(14); private static final long TWO_WEEKS_MS = TimeUnit.DAYS.toMillis(14); - @VisibleForTesting - static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3); + @VisibleForTesting static final long REQUEST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3); /** * If the user presses the {@link android.view.KeyEvent#KEYCODE_MEDIA_PREVIOUS} button within * this threshold from the program start time, the play position moves to the start of the - * previous program. - * Otherwise, the play position moves to the start of the current program. + * previous program. Otherwise, the play position moves to the start of the current program. * This value is specified in the UX document. */ private static final long PROGRAM_START_TIME_THRESHOLD = TimeUnit.SECONDS.toMillis(3); /** * If the current position enters within this range from the recording start time, rewind action - * and jump to previous action is disabled. - * Similarly, if the current position enters within this range from the current system time, - * fast forward action and jump to next action is disabled. - * It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at least. + * and jump to previous action is disabled. Similarly, if the current position enters within + * this range from the current system time, fast forward action and jump to next action is + * disabled. It must be three times longer than {@link #REQUEST_CURRENT_POSITION_INTERVAL} at + * least. */ private static final long DISABLE_ACTION_THRESHOLD = 3 * REQUEST_CURRENT_POSITION_INTERVAL; /** * If the current position goes out of this range from the recording start time, rewind action - * and jump to previous action is enabled. - * Similarly, if the current position goes out of this range from the current system time, - * fast forward action and jump to next action is enabled. - * Enable threshold and disable threshold must be different because the current position - * does not have the continuous value. It changes every one second. + * and jump to previous action is enabled. Similarly, if the current position goes out of this + * range from the current system time, fast forward action and jump to next action is enabled. + * Enable threshold and disable threshold must be different because the current position does + * not have the continuous value. It changes every one second. */ private static final long ENABLE_ACTION_THRESHOLD = DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL; /** - * The current position sent from TIS can not be exactly the same as the current system time - * due to the elapsed time to pass the message from TIS to Live TV. - * So the boundary threshold is necessary. - * The same goes for the recording start time. - * It's the same {@link #REQUEST_CURRENT_POSITION_INTERVAL}. + * The current position sent from TIS can not be exactly the same as the current system time due + * to the elapsed time to pass the message from TIS to Live TV. So the boundary threshold + * is necessary. The same goes for the recording start time. It's the same {@link + * #REQUEST_CURRENT_POSITION_INTERVAL}. */ private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL; private final PlayController mPlayController; private final ProgramManager mProgramManager; private final Tracker mTracker; + @VisibleForTesting final CurrentPositionMediator mCurrentPositionMediator = new CurrentPositionMediator(); private Listener mListener; private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener; - private int mEnabledActionIds = TIME_SHIFT_ACTION_ID_PLAY | TIME_SHIFT_ACTION_ID_PAUSE - | TIME_SHIFT_ACTION_ID_REWIND | TIME_SHIFT_ACTION_ID_FAST_FORWARD - | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; - @TimeShiftActionId - private int mLastActionId = 0; + private int mEnabledActionIds = + TIME_SHIFT_ACTION_ID_PLAY + | TIME_SHIFT_ACTION_ID_PAUSE + | TIME_SHIFT_ACTION_ID_REWIND + | TIME_SHIFT_ACTION_ID_FAST_FORWARD + | TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS + | TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT; + @TimeShiftActionId private int mLastActionId = 0; private final Context mContext; @@ -169,8 +178,11 @@ public class TimeShiftManager { private final Handler mHandler = new TimeShiftHandler(this); - public TimeShiftManager(Context context, TunableTvView tvView, - ProgramDataManager programDataManager, Tracker tracker, + public TimeShiftManager( + Context context, + TunableTvView tvView, + ProgramDataManager programDataManager, + Tracker tracker, OnCurrentProgramUpdatedListener onCurrentProgramUpdatedListener) { mContext = context; mPlayController = new PlayController(tvView); @@ -179,23 +191,17 @@ public class TimeShiftManager { mOnCurrentProgramUpdatedListener = onCurrentProgramUpdatedListener; } - /** - * Sets a listener which will receive events from this class. - */ + /** Sets a listener which will receive events from this class. */ public void setListener(Listener listener) { mListener = listener; } - /** - * Checks if the trick play is available for the current channel. - */ + /** Checks if the trick play is available for the current channel. */ public boolean isAvailable() { return mPlayController.mAvailable; } - /** - * Returns the current time position in milliseconds. - */ + /** Returns the current time position in milliseconds. */ public long getCurrentPositionMs() { return mCurrentPositionMediator.mCurrentPositionMs; } @@ -204,18 +210,15 @@ public class TimeShiftManager { mCurrentPositionMediator.onCurrentPositionChanged(currentTimeMs); } - /** - * Returns the start time of the recording in milliseconds. - */ + /** Returns the start time of the recording in milliseconds. */ public long getRecordStartTimeMs() { long oldestProgramStartTime = mProgramManager.getOldestProgramStartTime(); - return oldestProgramStartTime == INVALID_TIME ? INVALID_TIME + return oldestProgramStartTime == INVALID_TIME + ? INVALID_TIME : mPlayController.mRecordStartTimeMs; } - /** - * Returns the end time of the recording in milliseconds. - */ + /** Returns the end time of the recording in milliseconds. */ public long getRecordEndTimeMs() { if (mPlayController.mRecordEndTimeMs == CURRENT_TIME) { return System.currentTimeMillis(); @@ -264,9 +267,9 @@ public class TimeShiftManager { } /** - * Plays the media in backward direction. The playback speed is increased by 1x each time - * this is called. The range of the speed is from 2x to 5x. - * If the playing position is considered the same as the record start time, it does nothing + * Plays the media in backward direction. The playback speed is increased by 1x each time this + * is called. The range of the speed is from 2x to 5x. If the playing position is considered the + * same as the record start time, it does nothing * * @throws IllegalStateException if the trick play is not available. */ @@ -281,9 +284,9 @@ public class TimeShiftManager { } /** - * Plays the media in forward direction. The playback speed is increased by 1x each time - * this is called. The range of the speed is from 2x to 5x. - * If the playing position is the same as the current time, it does nothing. + * Plays the media in forward direction. The playback speed is increased by 1x each time this is + * called. The range of the speed is from 2x to 5x. If the playing position is the same as the + * current time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ @@ -298,11 +301,10 @@ public class TimeShiftManager { } /** - * Jumps to the start of the current program. - * If the currently playing position is within 3 seconds - * (={@link #PROGRAM_START_TIME_THRESHOLD})from the start time of the program, it goes to - * the start of the previous program if exists. - * If the playing position is the same as the record start time, it does nothing. + * Jumps to the start of the current program. If the currently playing position is within 3 + * seconds (={@link #PROGRAM_START_TIME_THRESHOLD})from the start time of the program, it goes + * to the start of the previous program if exists. If the playing position is the same as the + * record start time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ @@ -310,8 +312,9 @@ public class TimeShiftManager { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) { return; } - Program program = mProgramManager.getProgramAt( - mCurrentPositionMediator.mCurrentPositionMs - PROGRAM_START_TIME_THRESHOLD); + Program program = + mProgramManager.getProgramAt( + mCurrentPositionMediator.mCurrentPositionMs - PROGRAM_START_TIME_THRESHOLD); if (program == null) { return; } @@ -325,9 +328,9 @@ public class TimeShiftManager { } /** - * Jumps to the start of the next program if exists. - * If there's no next program, it jumps to the current system time and shows the live TV. - * If the playing position is considered the same as the current time, it does nothing. + * Jumps to the start of the next program if exists. If there's no next program, it jumps to the + * current system time and shows the live TV. If the playing position is considered the same as + * the current time, it does nothing. * * @throws IllegalStateException if the trick play is not available. */ @@ -335,8 +338,8 @@ public class TimeShiftManager { if (!isActionEnabled(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) { return; } - Program currentProgram = mProgramManager.getProgramAt( - mCurrentPositionMediator.mCurrentPositionMs); + Program currentProgram = + mProgramManager.getProgramAt(mCurrentPositionMediator.mCurrentPositionMs); if (currentProgram == null) { return; } @@ -362,10 +365,9 @@ public class TimeShiftManager { updateActions(); } - /** - * Returns the playback status. The value is PLAY_STATUS_PAUSED or PLAY_STATUS_PLAYING. - */ - @PlayStatus public int getPlayStatus() { + /** Returns the playback status. The value is PLAY_STATUS_PAUSED or PLAY_STATUS_PLAYING. */ + @PlayStatus + public int getPlayStatus() { return mPlayController.mPlayStatus; } @@ -373,27 +375,26 @@ public class TimeShiftManager { * Returns the displayed playback speed. The value is one of PLAY_SPEED_1X, PLAY_SPEED_2X, * PLAY_SPEED_3X, PLAY_SPEED_4X and PLAY_SPEED_5X. */ - @PlaySpeed public int getDisplayedPlaySpeed() { + @PlaySpeed + public int getDisplayedPlaySpeed() { return mPlayController.mDisplayedPlaySpeed; } /** * Returns the playback speed. The value is PLAY_DIRECTION_FORWARD or PLAY_DIRECTION_BACKWARD. */ - @PlayDirection public int getPlayDirection() { + @PlayDirection + public int getPlayDirection() { return mPlayController.mPlayDirection; } - /** - * Returns the ID of the last action.. - */ - @TimeShiftActionId public int getLastActionId() { + /** Returns the ID of the last action.. */ + @TimeShiftActionId + public int getLastActionId() { return mLastActionId; } - /** - * Enables or disables the time-shift actions. - */ + /** Enables or disables the time-shift actions. */ @VisibleForTesting void enableAction(@TimeShiftActionId int actionId, boolean enable) { int oldEnabledActionIds = mEnabledActionIds; @@ -402,8 +403,7 @@ public class TimeShiftManager { } else { mEnabledActionIds &= ~actionId; } - if (mNotificationEnabled && mListener != null - && oldEnabledActionIds != mEnabledActionIds) { + if (mNotificationEnabled && mListener != null && oldEnabledActionIds != mEnabledActionIds) { mListener.onActionEnabledChanged(actionId, enable); } } @@ -417,17 +417,22 @@ public class TimeShiftManager { enableAction(TIME_SHIFT_ACTION_ID_PLAY, true); enableAction(TIME_SHIFT_ACTION_ID_PAUSE, true); // Rewind action and jump to previous action. - long threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND) - ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD; - boolean enabled = mCurrentPositionMediator.mCurrentPositionMs - - mPlayController.mRecordStartTimeMs > threshold; + long threshold = + isActionEnabled(TIME_SHIFT_ACTION_ID_REWIND) + ? DISABLE_ACTION_THRESHOLD + : ENABLE_ACTION_THRESHOLD; + boolean enabled = + mCurrentPositionMediator.mCurrentPositionMs - mPlayController.mRecordStartTimeMs + > threshold; enableAction(TIME_SHIFT_ACTION_ID_REWIND, enabled); enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS, enabled); // Fast forward action and jump to next action - threshold = isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD) - ? DISABLE_ACTION_THRESHOLD : ENABLE_ACTION_THRESHOLD; - enabled = getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs - > threshold; + threshold = + isActionEnabled(TIME_SHIFT_ACTION_ID_FAST_FORWARD) + ? DISABLE_ACTION_THRESHOLD + : ENABLE_ACTION_THRESHOLD; + enabled = + getRecordEndTimeMs() - mCurrentPositionMediator.mCurrentPositionMs > threshold; enableAction(TIME_SHIFT_ACTION_ID_FAST_FORWARD, enabled); enableAction(TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT, enabled); } else { @@ -444,7 +449,7 @@ public class TimeShiftManager { SoftPreconditions.checkState(isAvailable(), TAG, "Time shift is not available"); SoftPreconditions.checkState(mCurrentPositionMediator.mCurrentPositionMs != INVALID_TIME); Program currentProgram = getProgramAt(mCurrentPositionMediator.mCurrentPositionMs); - if (!Program.isValid(currentProgram)) { + if (!Program.isProgramValid(currentProgram)) { currentProgram = null; } if (!Objects.equals(mCurrentProgram, currentProgram)) { @@ -453,8 +458,8 @@ public class TimeShiftManager { if (mNotificationEnabled && mOnCurrentProgramUpdatedListener != null) { Channel channel = mPlayController.getCurrentChannel(); if (channel != null) { - mOnCurrentProgramUpdatedListener.onCurrentProgramUpdated(channel.getId(), - mCurrentProgram); + mOnCurrentProgramUpdatedListener.onCurrentProgramUpdated( + channel.getId(), mCurrentProgram); mPlayController.onCurrentProgramChanged(); } } @@ -472,16 +477,12 @@ public class TimeShiftManager { && mPlayController.mDisplayedPlaySpeed == PLAY_SPEED_1X; } - /** - * Checks if the trick play is available and it's playback status is paused. - */ + /** Checks if the trick play is available and it's playback status is paused. */ public boolean isPaused() { return mPlayController.mAvailable && mPlayController.mPlayStatus == PLAY_STATUS_PAUSED; } - /** - * Returns the program which airs at the given time. - */ + /** Returns the program which airs at the given time. */ @NonNull public Program getProgramAt(long timeMs) { Program program = mProgramManager.getProgramAt(timeMs); @@ -495,8 +496,10 @@ public class TimeShiftManager { void onAvailabilityChanged() { mCurrentPositionMediator.initialize(mPlayController.mRecordStartTimeMs); - mProgramManager.onAvailabilityChanged(mPlayController.mAvailable, - mPlayController.getCurrentChannel(), mPlayController.mRecordStartTimeMs); + mProgramManager.onAvailabilityChanged( + mPlayController.mAvailable, + mPlayController.getCurrentChannel(), + mPlayController.mRecordStartTimeMs); updateActions(); // Availability change notification should be always sent // even if mNotificationEnabled is false. @@ -507,8 +510,8 @@ public class TimeShiftManager { void onRecordTimeRangeChanged() { if (mPlayController.mAvailable) { - mProgramManager.onRecordTimeRangeChanged(mPlayController.mRecordStartTimeMs, - mPlayController.mRecordEndTimeMs); + mProgramManager.onRecordTimeRangeChanged( + mPlayController.mRecordStartTimeMs, mPlayController.mRecordEndTimeMs); } updateActions(); if (mNotificationEnabled && mListener != null) { @@ -538,10 +541,10 @@ public class TimeShiftManager { } /** - * Returns the current program which airs right now.<p> + * Returns the current program which airs right now. * - * If the program is a dummy program, which means there's no program information, - * returns {@code null}. + * <p>If the program is a dummy program, which means there's no program information, returns + * {@code null}. */ @Nullable public Program getCurrentProgram() { @@ -558,8 +561,10 @@ public class TimeShiftManager { long durationMs = (getCurrentProgram() == null ? 0 : getCurrentProgram().getDurationMillis()); if (mPlayController.mDisplayedPlaySpeed > PLAY_SPEED_5X) { - Log.w(TAG, "Unknown displayed play speed is chosen : " - + mPlayController.mDisplayedPlaySpeed); + Log.w( + TAG, + "Unknown displayed play speed is chosen : " + + mPlayController.mDisplayedPlaySpeed); return TimeShiftUtils.getMaxPlaybackSpeed(durationMs); } else { return TimeShiftUtils.getPlaybackSpeed( @@ -568,9 +573,7 @@ public class TimeShiftManager { } } - /** - * A class which controls the trick play. - */ + /** A class which controls the trick play. */ private class PlayController { private final TunableTvView mTvView; @@ -585,69 +588,87 @@ public class TimeShiftManager { private boolean mAvailable; /** - * Indicates that the trick play is not playing the current time position. - * It is set true when {@link PlayController#pause}, {@link PlayController#rewind}, - * {@link PlayController#fastForward} and {@link PlayController#seekTo} - * is called. - * If it is true, the current time is equal to System.currentTimeMillis(). + * Indicates that the trick play is not playing the current time position. It is set true + * when {@link PlayController#pause}, {@link PlayController#rewind}, {@link + * PlayController#fastForward} and {@link PlayController#seekTo} is called. If it is true, + * the current time is equal to System.currentTimeMillis(). */ private boolean mIsPlayOffsetChanged; PlayController(TunableTvView tvView) { mTvView = tvView; - mTvView.setTimeShiftListener(new TimeShiftListener() { - @Override - public void onAvailabilityChanged() { - if (DEBUG) { - Log.d(TAG, "onAvailabilityChanged(available=" - + mTvView.isTimeShiftAvailable() + ")"); - } - PlayController.this.onAvailabilityChanged(); - } + mTvView.setTimeShiftListener( + new TimeShiftListener() { + @Override + public void onAvailabilityChanged() { + if (DEBUG) { + Log.d( + TAG, + "onAvailabilityChanged(available=" + + mTvView.isTimeShiftAvailable() + + ")"); + } + PlayController.this.onAvailabilityChanged(); + } - @Override - public void onRecordStartTimeChanged(long recordStartTimeMs) { - if (!SoftPreconditions.checkState(mAvailable, TAG, - "Trick play is not available.")) { - return; - } - if (recordStartTimeMs < mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) { - Log.e(TAG, "The start time is too earlier than the time of availability: {" - + "startTime: " + recordStartTimeMs + ", availability: " - + mAvailablityChangedTimeMs); - return; - } - if (recordStartTimeMs > System.currentTimeMillis()) { - // The time reported by TvInputService might not consistent with system - // clock,, use system's current time instead. - Log.e(TAG, "The start time should not be earlier than the current time, " - + "reset the start time to the system's current time: {" - + "startTime: " + recordStartTimeMs + ", current time: " - + System.currentTimeMillis()); - recordStartTimeMs = System.currentTimeMillis(); - } - if (mRecordStartTimeMs == recordStartTimeMs) { - return; - } - mRecordStartTimeMs = recordStartTimeMs; - TimeShiftManager.this.onRecordTimeRangeChanged(); - - // According to the UX guidelines, the stream should be resumed if the - // recording buffer fills up while paused, which means that the current time - // position is the same as or before the recording start time. - // But, for this application and the TIS, it's an erroneous and confusing - // situation if the current time position is before the recording start time. - // So, we recommend the TIS to keep the current time position greater than or - // equal to the recording start time. - // And here, we assume that the buffer is full if the current time position - // is nearly equal to the recording start time. - if (mPlayStatus == PLAY_STATUS_PAUSED && - getCurrentPositionMs() - mRecordStartTimeMs - < RECORDING_BOUNDARY_THRESHOLD) { - TimeShiftManager.this.play(); - } - } - }); + @Override + public void onRecordStartTimeChanged(long recordStartTimeMs) { + if (!SoftPreconditions.checkState( + mAvailable, TAG, "Trick play is not available.")) { + return; + } + if (recordStartTimeMs + < mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) { + Log.e( + TAG, + "The start time is too earlier than the time of availability: {" + + "startTime: " + + recordStartTimeMs + + ", availability: " + + mAvailablityChangedTimeMs); + return; + } + if (recordStartTimeMs > System.currentTimeMillis()) { + // The time reported by TvInputService might not consistent with + // system + // clock,, use system's current time instead. + Log.e( + TAG, + "The start time should not be earlier than the current time, " + + "reset the start time to the system's current time: {" + + "startTime: " + + recordStartTimeMs + + ", current time: " + + System.currentTimeMillis()); + recordStartTimeMs = System.currentTimeMillis(); + } + if (mRecordStartTimeMs == recordStartTimeMs) { + return; + } + mRecordStartTimeMs = recordStartTimeMs; + TimeShiftManager.this.onRecordTimeRangeChanged(); + + // According to the UX guidelines, the stream should be resumed if the + // recording buffer fills up while paused, which means that the current + // time + // position is the same as or before the recording start time. + // But, for this application and the TIS, it's an erroneous and + // confusing + // situation if the current time position is before the recording start + // time. + // So, we recommend the TIS to keep the current time position greater + // than or + // equal to the recording start time. + // And here, we assume that the buffer is full if the current time + // position + // is nearly equal to the recording start time. + if (mPlayStatus == PLAY_STATUS_PAUSED + && getCurrentPositionMs() - mRecordStartTimeMs + < RECORDING_BOUNDARY_THRESHOLD) { + TimeShiftManager.this.play(); + } + } + }); } void onAvailabilityChanged() { @@ -672,8 +693,8 @@ public class TimeShiftManager { mRecordEndTimeMs = CURRENT_TIME; // When the media availability message has come. mPlayController.setPlayStatus(PLAY_STATUS_PLAYING); - mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION, - REQUEST_CURRENT_POSITION_INTERVAL); + mHandler.sendEmptyMessageDelayed( + MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL); } else { mAvailablityChangedTimeMs = INVALID_TIME; mIsPlayOffsetChanged = false; @@ -688,11 +709,14 @@ public class TimeShiftManager { void handleGetCurrentPosition() { if (mIsPlayOffsetChanged) { - long currentTimeMs = mRecordEndTimeMs == CURRENT_TIME ? System.currentTimeMillis() - : mRecordEndTimeMs; - long currentPositionMs = Math.max( - Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs), - mRecordStartTimeMs); + long currentTimeMs = + mRecordEndTimeMs == CURRENT_TIME + ? System.currentTimeMillis() + : mRecordEndTimeMs; + long currentPositionMs = + Math.max( + Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs), + mRecordStartTimeMs); boolean isCurrentTime = currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD; long newCurrentPositionMs; @@ -708,8 +732,8 @@ public class TimeShiftManager { } } else { newCurrentPositionMs = currentPositionMs; - boolean isRecordStartTime = currentPositionMs - mRecordStartTimeMs - < RECORDING_BOUNDARY_THRESHOLD; + boolean isRecordStartTime = + currentPositionMs - mRecordStartTimeMs < RECORDING_BOUNDARY_THRESHOLD; if (isRecordStartTime && isRewinding()) { TimeShiftManager.this.play(); } @@ -721,8 +745,8 @@ public class TimeShiftManager { } // Need to send message here just in case there is no or invalid response // for the current time position request from TIS. - mHandler.sendEmptyMessageDelayed(MSG_GET_CURRENT_POSITION, - REQUEST_CURRENT_POSITION_INTERVAL); + mHandler.sendEmptyMessageDelayed( + MSG_GET_CURRENT_POSITION, REQUEST_CURRENT_POSITION_INTERVAL); } void play() { @@ -777,12 +801,13 @@ public class TimeShiftManager { mIsPlayOffsetChanged = true; } - /** - * Moves to the specified time. - */ + /** Moves to the specified time. */ void seekTo(long timeMs) { - mTvView.timeshiftSeekTo(Math.min(mRecordEndTimeMs == CURRENT_TIME - ? System.currentTimeMillis() : mRecordEndTimeMs, + mTvView.timeshiftSeekTo( + Math.min( + mRecordEndTimeMs == CURRENT_TIME + ? System.currentTimeMillis() + : mRecordEndTimeMs, Math.max(mRecordStartTimeMs, timeMs))); mIsPlayOffsetChanged = true; } @@ -853,8 +878,15 @@ public class TimeShiftManager { void onAvailabilityChanged(boolean available, Channel channel, long currentPositionMs) { if (DEBUG) { - Log.d(TAG, "onAvailabilityChanged(" + available + "+," + channel + ", " - + currentPositionMs + ")"); + Log.d( + TAG, + "onAvailabilityChanged(" + + available + + "+," + + channel + + ", " + + currentPositionMs + + ")"); } mProgramLoadQueue.clear(); @@ -875,12 +907,14 @@ public class TimeShiftManager { mPrograms.add(program); prefetchStartTimeMs = program.getEndTimeUtcMillis(); } else { - prefetchStartTimeMs = Utils.floorTime(currentPositionMs, - MAX_DUMMY_PROGRAM_DURATION); + prefetchStartTimeMs = + Utils.floorTime(currentPositionMs, MAX_DUMMY_PROGRAM_DURATION); } // Create dummy program - mPrograms.addAll(createDummyPrograms(prefetchStartTimeMs, - currentPositionMs + PREFETCH_DURATION_FOR_NEXT)); + mPrograms.addAll( + createDummyPrograms( + prefetchStartTimeMs, + currentPositionMs + PREFETCH_DURATION_FOR_NEXT)); schedulePrefetchPrograms(); TimeShiftManager.this.onProgramInfoChanged(); } @@ -895,8 +929,9 @@ public class TimeShiftManager { } long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); - long fetchEndTimeMs = Utils.ceilTime(endTimeMs + PREFETCH_DURATION_FOR_NEXT, - MAX_DUMMY_PROGRAM_DURATION); + long fetchEndTimeMs = + Utils.ceilTime( + endTimeMs + PREFETCH_DURATION_FOR_NEXT, MAX_DUMMY_PROGRAM_DURATION); removeOutdatedPrograms(fetchStartTimeMs); boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs); if (needToLoad) { @@ -934,16 +969,16 @@ public class TimeShiftManager { Range<Long> next = mProgramLoadQueue.poll(); // Extend next to include any overlapping Ranges. Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); - while(i.hasNext()) { + while (i.hasNext()) { Range<Long> r = i.next(); - if(next.contains(r.getLower()) || next.contains(r.getUpper())){ + if (next.contains(r.getLower()) || next.contains(r.getUpper())) { i.remove(); next = next.extend(r); } } if (mChannel != null) { - mProgramLoadTask = new LoadProgramsForCurrentChannelTask( - mContext.getContentResolver(), next); + mProgramLoadTask = + new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), next); mProgramLoadTask.executeOnDbThread(); } } @@ -969,10 +1004,12 @@ public class TimeShiftManager { if (!firstProgram.isValid()) { // Already the firstProgram is dummy. mPrograms.remove(0); - mPrograms.addAll(0, + mPrograms.addAll( + 0, createDummyPrograms(startTimeMs, firstProgram.getEndTimeUtcMillis())); } else { - mPrograms.addAll(0, + mPrograms.addAll( + 0, createDummyPrograms(startTimeMs, firstProgram.getStartTimeUtcMillis())); } added = true; @@ -1055,10 +1092,12 @@ public class TimeShiftManager { // to show the time-line duration of {@link MAX_DUMMY_PROGRAM_DURATION} at most // for a dummy program. private List<Program> createDummyPrograms(long startTimeMs, long endTimeMs) { - SoftPreconditions.checkArgument(endTimeMs - startTimeMs <= TWO_WEEKS_MS, TAG, - "createDummyProgram: long duration of dummy programs are requested (" - + Utils.toTimeString(startTimeMs) + ", " - + Utils.toTimeString(endTimeMs)); + SoftPreconditions.checkArgument( + endTimeMs - startTimeMs <= TWO_WEEKS_MS, + TAG, + "createDummyProgram: long duration of dummy programs are requested ( %s , %s)", + Utils.toTimeString(startTimeMs), + Utils.toTimeString(endTimeMs)); if (startTimeMs >= endTimeMs) { return Collections.emptyList(); } @@ -1066,17 +1105,19 @@ public class TimeShiftManager { long start = startTimeMs; long end = Utils.ceilTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION); while (end < endTimeMs) { - programs.add(new Program.Builder() - .setStartTimeUtcMillis(start) - .setEndTimeUtcMillis(end) - .build()); + programs.add( + new Program.Builder() + .setStartTimeUtcMillis(start) + .setEndTimeUtcMillis(end) + .build()); start = end; end += MAX_DUMMY_PROGRAM_DURATION; } - programs.add(new Program.Builder() - .setStartTimeUtcMillis(start) - .setEndTimeUtcMillis(endTimeMs) - .build()); + programs.add( + new Program.Builder() + .setStartTimeUtcMillis(start) + .setEndTimeUtcMillis(endTimeMs) + .build()); return programs; } @@ -1093,7 +1134,7 @@ public class TimeShiftManager { if (program.getStartTimeUtcMillis() > timeMs) { return getProgramAt(timeMs, start, mid - 1); } else if (program.getEndTimeUtcMillis() <= timeMs) { - return getProgramAt(timeMs, mid+1, end); + return getProgramAt(timeMs, mid + 1, end); } else { return program; } @@ -1125,8 +1166,10 @@ public class TimeShiftManager { if (DEBUG) Log.d(TAG, "Last valid program = " + lastValidProgram); final long delay; if (lastValidProgram != null) { - delay = lastValidProgram.getEndTimeUtcMillis() - - PREFETCH_TIME_OFFSET_FROM_PROGRAM_END - System.currentTimeMillis(); + delay = + lastValidProgram.getEndTimeUtcMillis() + - PREFETCH_TIME_OFFSET_FROM_PROGRAM_END + - System.currentTimeMillis(); } else { // Since there might not be any program data delay the retry 5 seconds, // then 30 seconds then 5 minutes @@ -1145,7 +1188,8 @@ public class TimeShiftManager { break; } if (DEBUG) { - Log.d(TAG, + Log.d( + TAG, "No last valid program. Already tried " + mEmptyFetchCount + " times"); } } @@ -1165,8 +1209,13 @@ public class TimeShiftManager { long endTimeMs = System.currentTimeMillis() + PREFETCH_DURATION_FOR_NEXT; if (startTimeMs <= endTimeMs) { if (DEBUG) { - Log.d(TAG, "Prefetch task starts: {startTime=" + Utils.toTimeString(startTimeMs) - + ", endTime=" + Utils.toTimeString(endTimeMs) + "}"); + Log.d( + TAG, + "Prefetch task starts: {startTime=" + + Utils.toTimeString(startTimeMs) + + ", endTime=" + + Utils.toTimeString(endTimeMs) + + "}"); } mProgramLoadQueue.add(Range.create(startTimeMs, endTimeMs)); } @@ -1176,20 +1225,28 @@ public class TimeShiftManager { private class LoadProgramsForCurrentChannelTask extends AsyncDbTask.LoadProgramsForChannelTask { - LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, - Range<Long> period) { - super(contentResolver, mChannel.getId(), period); + LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) { + super( + TvSingletons.getSingletons(mContext).getDbExecutor(), + contentResolver, + mChannel.getId(), + period); } @Override protected void onPostExecute(List<Program> programs) { if (DEBUG) { - Log.d(TAG, "Programs are loaded {channelId=" + mChannelId + - ", from=" + Utils.toTimeString(mPeriod.getLower()) + - ", to=" + Utils.toTimeString(mPeriod.getUpper()) + - "}"); + Log.d( + TAG, + "Programs are loaded {channelId=" + + mChannelId + + ", from=" + + Utils.toTimeString(mPeriod.getLower()) + + ", to=" + + Utils.toTimeString(mPeriod.getUpper()) + + "}"); } - //remove pending tasks that are fully satisfied by this query. + // remove pending tasks that are fully satisfied by this query. Iterator<Range<Long>> it = mProgramLoadQueue.iterator(); while (it.hasNext()) { Range<Long> r = it.next(); @@ -1207,14 +1264,14 @@ public class TimeShiftManager { return; } mEmptyFetchCount = 0; - if(!mPrograms.isEmpty()) { + if (!mPrograms.isEmpty()) { removeDummyPrograms(); removeOverlappedPrograms(programs); Program loadedProgram = programs.get(0); for (int i = 0; i < mPrograms.size() && !programs.isEmpty(); ++i) { Program program = mPrograms.get(i); - while (program.getStartTimeUtcMillis() > loadedProgram - .getStartTimeUtcMillis()) { + while (program.getStartTimeUtcMillis() + > loadedProgram.getStartTimeUtcMillis()) { mPrograms.add(i++, loadedProgram); programs.remove(0); if (programs.isEmpty()) { @@ -1234,10 +1291,15 @@ public class TimeShiftManager { @Override protected void onCancelled(List<Program> programs) { if (DEBUG) { - Log.d(TAG, "Program loading has been canceled {channelId=" + (mChannel == null - ? "null" : mChannelId) + ", from=" + Utils - .toTimeString(mPeriod.getLower()) + ", to=" + Utils - .toTimeString(mPeriod.getUpper()) + "}"); + Log.d( + TAG, + "Program loading has been canceled {channelId=" + + (mChannel == null ? "null" : mChannelId) + + ", from=" + + Utils.toTimeString(mPeriod.getLower()) + + ", to=" + + Utils.toTimeString(mPeriod.getUpper()) + + "}"); } startNextLoadingIfNeeded(); } @@ -1247,12 +1309,13 @@ public class TimeShiftManager { mProgramLoadTask = null; } // Need to post to handler, because the task is still running. - mHandler.post(new Runnable() { - @Override - public void run() { - startTaskIfNeeded(); - } - }); + mHandler.post( + new Runnable() { + @Override + public void run() { + startTaskIfNeeded(); + } + }); } boolean overlaps(Queue<Range<Long>> programLoadQueue) { @@ -1299,11 +1362,11 @@ public class TimeShiftManager { } else { if (getPlayStatus() == PLAY_STATUS_PLAYING) { if (getPlayDirection() == PLAY_DIRECTION_FORWARD) { - mCurrentPositionMs += (currentTimeMs - mSeekRequestTimeMs) - * getPlaybackSpeed(); + mCurrentPositionMs += + (currentTimeMs - mSeekRequestTimeMs) * getPlaybackSpeed(); } else { - mCurrentPositionMs -= (currentTimeMs - mSeekRequestTimeMs) - * getPlaybackSpeed(); + mCurrentPositionMs -= + (currentTimeMs - mSeekRequestTimeMs) * getPlaybackSpeed(); } } TimeShiftManager.this.onCurrentPositionChanged(); @@ -1311,9 +1374,7 @@ public class TimeShiftManager { } } - /** - * The listener used to receive the events by the time-shift manager - */ + /** The listener used to receive the events by the time-shift manager */ public interface Listener { /** * Called when the availability of the time-shift for the current channel has been changed. @@ -1323,31 +1384,23 @@ public class TimeShiftManager { void onAvailabilityChanged(); /** - * Called when the play status is changed between {@link #PLAY_STATUS_PLAYING} and - * {@link #PLAY_STATUS_PAUSED} + * Called when the play status is changed between {@link #PLAY_STATUS_PLAYING} and {@link + * #PLAY_STATUS_PAUSED} * * @param status The new play state. */ void onPlayStatusChanged(int status); - /** - * Called when the recordStartTime has been changed. - */ + /** Called when the recordStartTime has been changed. */ void onRecordTimeRangeChanged(); - /** - * Called when the current position is changed. - */ + /** Called when the current position is changed. */ void onCurrentPositionChanged(); - /** - * Called when the program information is updated. - */ + /** Called when the program information is updated. */ void onProgramInfoChanged(); - /** - * Called when an action becomes enabled or disabled. - */ + /** Called when an action becomes enabled or disabled. */ void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled); } diff --git a/src/com/android/tv/TvActivity.java b/src/com/android/tv/TvActivity.java index 9a1cea55..aa0f0e8c 100644 --- a/src/com/android/tv/TvActivity.java +++ b/src/com/android/tv/TvActivity.java @@ -18,7 +18,6 @@ package com.android.tv; import android.app.Activity; import android.content.Intent; - import com.android.tv.util.Utils; public class TvActivity extends Activity { diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index 0c7c0fd1..826317b9 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -18,11 +18,9 @@ package com.android.tv; import android.annotation.TargetApi; import android.app.Activity; -import android.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -30,32 +28,25 @@ import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.StrictMode; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; - -import com.android.tv.analytics.Analytics; -import com.android.tv.analytics.StubAnalytics; -import com.android.tv.analytics.StubAnalytics; -import com.android.tv.analytics.Tracker; -import com.android.tv.common.BuildConfig; -import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.common.BaseApplication; +import com.android.tv.common.concurrent.NamedThreadFactory; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; -import com.android.tv.config.DefaultConfigManager; -import com.android.tv.config.RemoteConfig; +import com.android.tv.common.util.Clock; +import com.android.tv.common.util.Debug; +import com.android.tv.common.util.SharedPreferencesUtils; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.PreviewDataManager; import com.android.tv.data.ProgramDataManager; import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.data.epg.EpgFetcherImpl; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManagerImpl; import com.android.tv.dvr.DvrManager; @@ -63,93 +54,81 @@ import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; -import com.android.tv.perf.EventNames; -import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.StubPerformanceMonitor; -import com.android.tv.perf.TimerEvent; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.recommendation.RecordedProgramPreviewUpdater; import com.android.tv.tuner.TunerInputController; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.tuner.util.TunerInputInfoUtils; -import com.android.tv.util.AccountHelper; -import com.android.tv.util.Clock; -import com.android.tv.util.Debug; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.SetupUtils; -import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -public class TvApplication extends Application implements ApplicationSingletons { +/** + * Live TV application. + * + * <p>This includes all the Google specific hooks. + */ +public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter { private static final String TAG = "TvApplication"; private static final boolean DEBUG = false; - private static final TimerEvent sAppStartTimer = StubPerformanceMonitor.startBootstrapTimer(); - /** - * An instance of {@link ApplicationSingletons}. Note that this can be set directly only for the - * test purpose. - */ - @VisibleForTesting - public static ApplicationSingletons sAppSingletons; + /** Namespace for LiveChannels configs. LiveChannels configs are kept in piper. */ + public static final String CONFIGNS_P4 = "configns:p4"; /** - * Broadcast Action: The user has updated LC to a new version that supports tuner input. - * {@link com.android.tv.tuner.TunerInputController} will recevice this intent to check - * the existence of tuner input when the new version is first launched. + * Broadcast Action: The user has updated LC to a new version that supports tuner input. {@link + * TunerInputController} will receive this intent to check the existence of tuner input when the + * new version is first launched. */ public static final String ACTION_APPLICATION_FIRST_LAUNCHED = - "com.android.tv.action.APPLICATION_FIRST_LAUNCHED"; + " com.android.tv.action.APPLICATION_FIRST_LAUNCHED"; + private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch"; - private RemoteConfig mRemoteConfig; + private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db"); + private static final ExecutorService DB_EXECUTOR = + Executors.newSingleThreadExecutor(THREAD_FACTORY); + private String mVersionName = ""; private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper(); private SelectInputActivity mSelectInputActivity; - private Analytics mAnalytics; - private Tracker mTracker; - private TvInputManagerHelper mTvInputManagerHelper; private ChannelDataManager mChannelDataManager; private volatile ProgramDataManager mProgramDataManager; private PreviewDataManager mPreviewDataManager; private DvrManager mDvrManager; private DvrScheduleManager mDvrScheduleManager; private DvrDataManager mDvrDataManager; - private DvrStorageStatusManager mDvrStorageStatusManager; private DvrWatchedPositionManager mDvrWatchedPositionManager; private RecordingScheduler mRecordingScheduler; - @Nullable - private InputSessionManager mInputSessionManager; - private AccountHelper mAccountHelper; + private RecordingStorageStatusManager mDvrStorageStatusManager; + @Nullable private InputSessionManager mInputSessionManager; + // STOP-SHIP: Remove this variable when Tuner Process is split to another application. // When this variable is null, we don't know in which process TvApplication runs. private Boolean mRunningInMainProcess; - private PerformanceMonitor mPerformanceMonitor; + private TvInputManagerHelper mTvInputManagerHelper; + private boolean mStarted; + private EpgFetcher mEpgFetcher; + private TunerInputController mTunerInputController; @Override public void onCreate() { super.onCreate(); - if (!PermissionUtils.hasInternet(this)) { - // When an isolated process starts, just skip all the initialization. - return; - } - Debug.getTimer(Debug.TAG_START_UP_TIMER).start(); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start TvApplication.onCreate"); - SharedPreferencesUtils.initialize(this, new Runnable() { - @Override - public void run() { - if (mRunningInMainProcess != null && mRunningInMainProcess) { - checkTunerServiceOnFirstLaunch(); - } - } - }); - // TunerPreferences is used to enable/disable the tuner input even when TUNER feature is - // disabled. - TunerPreferences.initialize(this); + SharedPreferencesUtils.initialize( + this, + new Runnable() { + @Override + public void run() { + if (mRunningInMainProcess != null && mRunningInMainProcess) { + checkTunerServiceOnFirstLaunch(); + } + } + }); try { PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); mVersionName = pInfo.versionName; @@ -159,73 +138,47 @@ public class TvApplication extends Application implements ApplicationSingletons } Log.i(TAG, "Starting Live TV " + getVersionName()); - // Only set StrictMode for ENG builds because the build server only produces userdebug - // builds. - if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { - StrictMode.ThreadPolicy.Builder threadPolicyBuilder = - new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog(); - StrictMode.VmPolicy.Builder vmPolicyBuilder = - new StrictMode.VmPolicy.Builder().detectAll().penaltyDeath(); - if (!TvCommonUtils.isRunningInTest()) { - threadPolicyBuilder.penaltyDialog(); - } - StrictMode.setThreadPolicy(threadPolicyBuilder.build()); - StrictMode.setVmPolicy(vmPolicyBuilder.build()); - } - if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) { - mAnalytics = StubAnalytics.getInstance(this); - } else { - mAnalytics = StubAnalytics.getInstance(this); - } - mTracker = mAnalytics.getDefaultTracker(); - getTvInputManagerHelper(); // In SetupFragment, transitions are set in the constructor. Because the fragment can be // created in Activity.onCreate() by the framework, SetupAnimationHelper should be // initialized here before Activity.onCreate() is called. + mEpgFetcher = EpgFetcherImpl.create(this); SetupAnimationHelper.initialize(this); - + getTvInputManagerHelper(); Log.i(TAG, "Started Live TV " + mVersionName); Debug.getTimer(Debug.TAG_START_UP_TIMER).log("finish TvApplication.onCreate"); - getPerformanceMonitor().stopTimer(sAppStartTimer, EventNames.APPLICATION_ONCREATE); } - private void setCurrentRunningProcess(boolean isMainProcess) { - if (mRunningInMainProcess != null) { - SoftPreconditions.checkState(isMainProcess == mRunningInMainProcess); + /** Initializes application. It is a noop if called twice. */ + @Override + public void start() { + if (mStarted) { return; } - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "start TvApplication.setCurrentRunningProcess"); - mRunningInMainProcess = isMainProcess; - if (CommonFeatures.DVR.isEnabled(this)) { - mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess); - } - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - // Fetch remote config - getRemoteConfig().fetch(null); - return null; - } - }.execute(); + mStarted = true; + mRunningInMainProcess = true; + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("start TvApplication.start"); if (mRunningInMainProcess) { - getTvInputManagerHelper().addCallback(new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - if (Features.TUNER.isEnabled(TvApplication.this) && TextUtils.equals(inputId, - TunerTvInputService.getInputId(TvApplication.this))) { - TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this); - } - handleInputCountChanged(); - } - - @Override - public void onInputRemoved(String inputId) { - handleInputCountChanged(); - } - }); - if (Features.TUNER.isEnabled(this)) { + getTvInputManagerHelper() + .addCallback( + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (TvFeatures.TUNER.isEnabled(TvApplication.this) + && TextUtils.equals( + inputId, getEmbeddedTunerInputId())) { + TunerInputInfoUtils.updateTunerInputInfo( + TvApplication.this); + } + handleInputCountChanged(); + } + + @Override + public void onInputRemoved(String inputId) { + handleInputCountChanged(); + } + }); + if (TvFeatures.TUNER.isEnabled(this)) { // If the tuner input service is added before the app is started, we need to // handle it here. TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this); @@ -235,58 +188,61 @@ public class TvApplication extends Application implements ApplicationSingletons mDvrManager = new DvrManager(this); mRecordingScheduler = RecordingScheduler.createScheduler(this); } - EpgFetcher.getInstance(this).startRoutineService(); + mEpgFetcher.startRoutineService(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ChannelPreviewUpdater.getInstance(this).startRoutineService(); RecordedProgramPreviewUpdater.getInstance(this) .updatePreviewDataForRecordedPrograms(); } } - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "finish TvApplication.setCurrentRunningProcess"); + Debug.getTimer(Debug.TAG_START_UP_TIMER).log("finish TvApplication.start"); } private void checkTunerServiceOnFirstLaunch() { - SharedPreferences sharedPreferences = this.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); + SharedPreferences sharedPreferences = + this.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_FEATURES, Context.MODE_PRIVATE); boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true); if (isFirstLaunch) { if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!"); - TunerInputController.onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED); + getTunerInputController() + .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false); editor.apply(); } } - /** - * Returns the {@link DvrManager}. - */ + @Override + public EpgFetcher getEpgFetcher() { + return mEpgFetcher; + } + + @Override + public synchronized SetupUtils getSetupUtils() { + return SetupUtils.createForTvSingletons(this); + } + + /** Returns the {@link DvrManager}. */ @Override public DvrManager getDvrManager() { return mDvrManager; } - /** - * Returns the {@link DvrScheduleManager}. - */ + /** Returns the {@link DvrScheduleManager}. */ @Override public DvrScheduleManager getDvrScheduleManager() { return mDvrScheduleManager; } - /** - * Returns the {@link RecordingScheduler}. - */ + /** Returns the {@link RecordingScheduler}. */ @Override @Nullable public RecordingScheduler getRecordingScheduler() { return mRecordingScheduler; } - /** - * Returns the {@link DvrWatchedPositionManager}. - */ + /** Returns the {@link DvrWatchedPositionManager}. */ @Override public DvrWatchedPositionManager getDvrWatchedPositionManager() { if (mDvrWatchedPositionManager == null) { @@ -304,25 +260,7 @@ public class TvApplication extends Application implements ApplicationSingletons return mInputSessionManager; } - /** - * Returns the {@link Analytics}. - */ - @Override - public Analytics getAnalytics() { - return mAnalytics; - } - - /** - * Returns the default tracker. - */ - @Override - public Tracker getTracker() { - return mTracker; - } - - /** - * Returns {@link ChannelDataManager}. - */ + /** Returns {@link ChannelDataManager}. */ @Override public ChannelDataManager getChannelDataManager() { if (mChannelDataManager == null) { @@ -337,23 +275,22 @@ public class TvApplication extends Application implements ApplicationSingletons return mChannelDataManager != null && mChannelDataManager.isDbLoadFinished(); } - /** - * Returns {@link ProgramDataManager}. - */ + /** Returns {@link ProgramDataManager}. */ @Override public ProgramDataManager getProgramDataManager() { if (mProgramDataManager != null) { return mProgramDataManager; } - Utils.runInMainThreadAndWait(new Runnable() { - @Override - public void run() { - if (mProgramDataManager == null) { - mProgramDataManager = new ProgramDataManager(TvApplication.this); - mProgramDataManager.start(); - } - } - }); + Utils.runInMainThreadAndWait( + new Runnable() { + @Override + public void run() { + if (mProgramDataManager == null) { + mProgramDataManager = new ProgramDataManager(TvApplication.this); + mProgramDataManager.start(); + } + } + }); return mProgramDataManager; } @@ -362,9 +299,7 @@ public class TvApplication extends Application implements ApplicationSingletons return mProgramDataManager != null && mProgramDataManager.isCurrentProgramsLoadFinished(); } - /** - * Returns {@link PreviewDataManager}. - */ + /** Returns {@link PreviewDataManager}. */ @TargetApi(Build.VERSION_CODES.O) @Override public PreviewDataManager getPreviewDataManager() { @@ -375,9 +310,7 @@ public class TvApplication extends Application implements ApplicationSingletons return mPreviewDataManager; } - /** - * Returns {@link DvrDataManager}. - */ + /** Returns {@link DvrDataManager}. */ @TargetApi(Build.VERSION_CODES.N) @Override public DvrDataManager getDvrDataManager() { @@ -389,53 +322,39 @@ public class TvApplication extends Application implements ApplicationSingletons return mDvrDataManager; } - /** - * Returns {@link DvrStorageStatusManager}. - */ - @TargetApi(Build.VERSION_CODES.N) - @Override - public DvrStorageStatusManager getDvrStorageStatusManager() { - return mDvrStorageStatusManager; - } - - /** - * Returns {@link TvInputManagerHelper}. - */ @Override - public TvInputManagerHelper getTvInputManagerHelper() { - if (mTvInputManagerHelper == null) { - mTvInputManagerHelper = new TvInputManagerHelper(this); - mTvInputManagerHelper.start(); + @TargetApi(Build.VERSION_CODES.N) + public RecordingStorageStatusManager getRecordingStorageStatusManager() { + if (mDvrStorageStatusManager == null) { + mDvrStorageStatusManager = new DvrStorageStatusManager(this); } - return mTvInputManagerHelper; + return mDvrStorageStatusManager; } - /** - * Returns the main activity information. - */ + /** Returns the main activity information. */ @Override public MainActivityWrapper getMainActivityWrapper() { return mMainActivityWrapper; } - /** - * Returns the {@link AccountHelper}. - */ + /** Returns {@link TvInputManagerHelper}. */ @Override - public AccountHelper getAccountHelper() { - if (mAccountHelper == null) { - mAccountHelper = new AccountHelper(getApplicationContext()); + public TvInputManagerHelper getTvInputManagerHelper() { + if (mTvInputManagerHelper == null) { + mTvInputManagerHelper = new TvInputManagerHelper(this); + mTvInputManagerHelper.start(); } - return mAccountHelper; + return mTvInputManagerHelper; } @Override - public RemoteConfig getRemoteConfig() { - if (mRemoteConfig == null) { - // No need to synchronize this, it does not hurt to create two and throw one away. - mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig(); + public synchronized TunerInputController getTunerInputController() { + if (mTunerInputController == null) { + mTunerInputController = + new TunerInputController( + ComponentName.unflattenFromString(getEmbeddedTunerInputId())); } - return mRemoteConfig; + return mTunerInputController; } @Override @@ -443,17 +362,9 @@ public class TvApplication extends Application implements ApplicationSingletons return mRunningInMainProcess != null && mRunningInMainProcess; } - @Override - public PerformanceMonitor getPerformanceMonitor() { - if (mPerformanceMonitor == null) { - mPerformanceMonitor = StubPerformanceMonitor.initialize(this); - } - return mPerformanceMonitor; - } - /** - * SelectInputActivity is set in {@link SelectInputActivity#onCreate} and cleared in - * {@link SelectInputActivity#onDestroy}. + * SelectInputActivity is set in {@link SelectInputActivity#onCreate} and cleared in {@link + * SelectInputActivity#onDestroy}. */ public void setSelectInputActivity(SelectInputActivity activity) { mSelectInputActivity = activity; @@ -467,18 +378,19 @@ public class TvApplication extends Application implements ApplicationSingletons } } - /** - * Handles the global key KEYCODE_TV. - */ + /** Handles the global key KEYCODE_TV. */ public void handleTvKey() { if (!mMainActivityWrapper.isResumed()) { startMainActivity(null); } } - /** - * Handles the global key KEYCODE_TV_INPUT. - */ + /** Handles the global key KEYCODE_DVR. */ + public void handleDvrKey() { + startActivity(new Intent(this, DvrBrowseActivity.class)); + } + + /** Handles the global key KEYCODE_TV_INPUT. */ public void handleTvInputKey() { TvInputManager tvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); List<TvInputInfo> tvInputs = tvInputManager.getTvInputList(); @@ -497,22 +409,25 @@ public class TvApplication extends Application implements ApplicationSingletons if (inputCount < 2) { return; } - Activity activityToHandle = mMainActivityWrapper.isResumed() - ? mMainActivityWrapper.getMainActivity() : mSelectInputActivity; + Activity activityToHandle = + mMainActivityWrapper.isResumed() + ? mMainActivityWrapper.getMainActivity() + : mSelectInputActivity; if (activityToHandle != null) { // If startActivity is called, MainActivity.onPause is unnecessarily called. To // prevent it, MainActivity.dispatchKeyEvent is directly called. activityToHandle.dispatchKeyEvent( new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TV_INPUT)); - activityToHandle.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, - KeyEvent.KEYCODE_TV_INPUT)); + activityToHandle.dispatchKeyEvent( + new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TV_INPUT)); } else if (mMainActivityWrapper.isStarted()) { Bundle extras = new Bundle(); extras.putString(Utils.EXTRA_KEY_ACTION, Utils.EXTRA_ACTION_SHOW_TV_INPUT); startMainActivity(extras); } else { - startActivity(new Intent(this, SelectInputActivity.class).setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK)); + startActivity( + new Intent(this, SelectInputActivity.class) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } } @@ -520,8 +435,8 @@ public class TvApplication extends Application implements ApplicationSingletons // The use of FLAG_ACTIVITY_NEW_TASK enables arbitrary applications to access the intent // sent to the root activity. Having said that, we should be fine here since such an intent // does not carry any important user data. - Intent intent = new Intent(this, MainActivity.class) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent intent = + new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (extras != null) { intent.putExtras(extras); } @@ -538,36 +453,39 @@ public class TvApplication extends Application implements ApplicationSingletons } /** - * Checks the input counts and enable/disable TvActivity. Also updates the input list in - * {@link SetupUtils}. + * Checks the input counts and enable/disable TvActivity. Also upda162 the input list in {@link + * SetupUtils}. */ + @Override public void handleInputCountChanged() { handleInputCountChanged(false, false, false); } /** - * Checks the input counts and enable/disable TvActivity. Also updates the input list in - * {@link SetupUtils}. + * Checks the input counts and enable/disable TvActivity. Also updates the input list in {@link + * SetupUtils}. * - * @param calledByTunerServiceChanged true if it is called when TunerTvInputService - * is enabled or disabled. + * @param calledByTunerServiceChanged true if it is called when BaseTunerTvInputService is + * enabled or disabled. * @param tunerServiceEnabled it's available only when calledByTunerServiceChanged is true. - * @param dontKillApp when TvActivity is enabled or disabled by this method, the app restarts - * by default. But, if dontKillApp is true, the app won't restart. + * @param dontKillApp when TvActivity is enabled or disabled by this method, the app restarts by + * default. But, if dontKillApp is true, the app won't restart. */ - public void handleInputCountChanged(boolean calledByTunerServiceChanged, - boolean tunerServiceEnabled, boolean dontKillApp) { + public void handleInputCountChanged( + boolean calledByTunerServiceChanged, boolean tunerServiceEnabled, boolean dontKillApp) { TvInputManager inputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); - boolean enable = (calledByTunerServiceChanged && tunerServiceEnabled) - || Features.UNHIDE.isEnabled(TvApplication.this); + boolean enable = + (calledByTunerServiceChanged && tunerServiceEnabled) + || TvFeatures.UNHIDE.isEnabled(TvApplication.this); if (!enable) { List<TvInputInfo> inputs = inputManager.getTvInputList(); boolean skipTunerInputCheck = false; // Enable the TvActivity only if there is at least one tuner type input. if (!skipTunerInputCheck) { for (TvInputInfo input : inputs) { - if (calledByTunerServiceChanged && !tunerServiceEnabled - && TunerTvInputService.getInputId(this).equals(input.getId())) { + if (calledByTunerServiceChanged + && !tunerServiceEnabled + && getEmbeddedTunerInputId().equals(input.getId())) { continue; } if (input.getType() == TvInputInfo.TYPE_TUNER) { @@ -580,43 +498,20 @@ public class TvApplication extends Application implements ApplicationSingletons } PackageManager packageManager = getPackageManager(); ComponentName name = new ComponentName(this, TvActivity.class); - int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + int newState = + enable + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; if (packageManager.getComponentEnabledSetting(name) != newState) { - packageManager.setComponentEnabledSetting(name, newState, - dontKillApp ? PackageManager.DONT_KILL_APP : 0); + packageManager.setComponentEnabledSetting( + name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0); Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV."); } - SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager); - } - - /** - * Returns the @{@link ApplicationSingletons} using the application context. - */ - public static ApplicationSingletons getSingletons(Context context) { - // No need to be "synchronized" because this doesn't create any instance. - if (sAppSingletons == null) { - sAppSingletons = (ApplicationSingletons) context.getApplicationContext(); - } - return sAppSingletons; + getSetupUtils().onInputListUpdated(inputManager); } - /** - * Sets true, if TvApplication is running on the main process. If TvApplication runs on - * tuner process or other process, it sets false. - * - * Note: it should be called at the beginning of Service.onCreate Activity.onCreate, or - * BroadcastReceiver.onCreate. When it is firstly called after launch, it runs process - * specific initializations. - */ - public static void setCurrentRunningProcess(Context context, boolean isMainProcess) { - // TODO(b/63064354) TvApplication should not have to know if it is "the main process" - if (context.getApplicationContext() instanceof TvApplication) { - TvApplication tvApplication = (TvApplication) context.getApplicationContext(); - tvApplication.setCurrentRunningProcess(isMainProcess); - } else { - // Application context can be MockTvApplication. - Log.w(TAG, "It is not a context of TvApplication"); - } + @Override + public Executor getDbExecutor() { + return DB_EXECUTOR; } } diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/TvFeatures.java new file mode 100644 index 00000000..d2cf76e7 --- /dev/null +++ b/src/com/android/tv/TvFeatures.java @@ -0,0 +1,104 @@ +/* + * 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; + +import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE; +import static com.android.tv.common.feature.FeatureUtils.AND; +import static com.android.tv.common.feature.FeatureUtils.OFF; +import static com.android.tv.common.feature.FeatureUtils.ON; +import static com.android.tv.common.feature.FeatureUtils.OR; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.support.annotation.VisibleForTesting; +import com.android.tv.common.experiments.Experiments; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.feature.ExperimentFeature; +import com.android.tv.common.feature.Feature; +import com.android.tv.common.feature.FeatureUtils; +import com.android.tv.common.feature.GServiceFeature; +import com.android.tv.common.feature.PropertyFeature; +import com.android.tv.common.feature.Sdk; +import com.android.tv.common.feature.TestableFeature; +import com.android.tv.common.util.PermissionUtils; + +import com.google.android.tv.partner.support.PartnerCustomizations; + +/** + * List of {@link Feature} for the Live TV App. + * + * <p>Remove the {@code Feature} once it is launched. + */ +public final class TvFeatures extends CommonFeatures { + + /** When enabled use system setting for turning on analytics. */ + public static final Feature ANALYTICS_OPT_IN = + ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX); + /** When enabled shows a list of failed recordings */ + public static final Feature DVR_FAILED_LIST = ENG_ONLY_FEATURE; + /** + * Analytics that include sensitive information such as channel or program identifiers. + * + * <p>See <a href="http://b/22062676">b/22062676</a> + */ + public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN); + + public static final Feature EPG_SEARCH = + PropertyFeature.create("feature_tv_use_epg_search", false); + + private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide"; + /** A flag which indicates that LC app is unhidden even when there is no input. */ + public static final Feature UNHIDE = + OR( + new GServiceFeature(GSERVICE_KEY_UNHIDE, false), + new Feature() { + @Override + public boolean isEnabled(Context context) { + // If LC app runs as non-system app, we unhide the app. + return !PermissionUtils.hasAccessAllEpg(context); + } + }); + + public static final Feature PICTURE_IN_PICTURE = + new Feature() { + private Boolean mEnabled; + + @Override + public boolean isEnabled(Context context) { + if (mEnabled == null) { + mEnabled = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && context.getPackageManager() + .hasSystemFeature( + PackageManager.FEATURE_PICTURE_IN_PICTURE); + } + return mEnabled; + } + }; + + /** Enable a conflict dialog between currently watched channel and upcoming recording. */ + public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF; + + /** Use input blacklist to disable partner's tuner input. */ + public static final Feature USE_PARTNER_INPUT_BLACKLIST = ON; + + @VisibleForTesting + public static final Feature TEST_FEATURE = PropertyFeature.create("test_feature", false); + + private TvFeatures() {} +} diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java index 493e039c..4e0636ff 100644 --- a/src/com/android/tv/TvOptionsManager.java +++ b/src/com/android/tv/TvOptionsManager.java @@ -20,9 +20,7 @@ import android.content.Context; import android.media.tv.TvTrackInfo; import android.support.annotation.IntDef; import android.util.SparseArray; - import com.android.tv.data.DisplayMode; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; @@ -33,9 +31,17 @@ import java.util.Locale; */ public class TvOptionsManager { @Retention(RetentionPolicy.SOURCE) - @IntDef({OPTION_CLOSED_CAPTIONS, OPTION_DISPLAY_MODE, OPTION_SYSTEMWIDE_PIP, OPTION_MULTI_AUDIO, - OPTION_MORE_CHANNELS, OPTION_DEVELOPER, OPTION_SETTINGS}) + @IntDef({ + OPTION_CLOSED_CAPTIONS, + OPTION_DISPLAY_MODE, + OPTION_SYSTEMWIDE_PIP, + OPTION_MULTI_AUDIO, + OPTION_MORE_CHANNELS, + OPTION_DEVELOPER, + OPTION_SETTINGS + }) public @interface OptionType {} + public static final int OPTION_CLOSED_CAPTIONS = 0; public static final int OPTION_DISPLAY_MODE = 1; public static final int OPTION_SYSTEMWIDE_PIP = 2; @@ -57,6 +63,7 @@ public class TvOptionsManager { /** * Returns a suitable displayed string for the given option type under current settings. + * * @param option the type of option, should be one of {@link OptionType}. */ public String getOptionString(@OptionType int option) { @@ -67,8 +74,9 @@ public class TvOptionsManager { } return new Locale(mClosedCaptionsLanguage).getDisplayName(); case OPTION_DISPLAY_MODE: - return ((MainActivity) mContext).getTvViewUiManager() - .isDisplayModeAvailable(mDisplayMode) + return ((MainActivity) mContext) + .getTvViewUiManager() + .isDisplayModeAvailable(mDisplayMode) ? DisplayMode.getLabel(mDisplayMode, mContext) : DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext); case OPTION_MULTI_AUDIO: @@ -77,27 +85,25 @@ public class TvOptionsManager { return ""; } - /** - * Handles changing selection of closed caption. - */ + /** Handles changing selection of closed caption. */ public void onClosedCaptionsChanged(TvTrackInfo track, int trackIndex) { - mClosedCaptionsLanguage = (track == null) ? - null : (track.getLanguage() != null) ? track.getLanguage() - : mContext.getString(R.string.closed_caption_unknown_language, trackIndex + 1); + mClosedCaptionsLanguage = + (track == null) + ? null + : (track.getLanguage() != null) + ? track.getLanguage() + : mContext.getString( + R.string.closed_caption_unknown_language, trackIndex + 1); notifyOptionChanged(OPTION_CLOSED_CAPTIONS); } - /** - * Handles changing selection of display mode. - */ + /** Handles changing selection of display mode. */ public void onDisplayModeChanged(int displayMode) { mDisplayMode = displayMode; notifyOptionChanged(OPTION_DISPLAY_MODE); } - /** - * Handles changing selection of multi-audio. - */ + /** Handles changing selection of multi-audio. */ public void onMultiAudioChanged(String multiAudio) { mMultiAudio = multiAudio; notifyOptionChanged(OPTION_MULTI_AUDIO); @@ -110,17 +116,13 @@ public class TvOptionsManager { } } - /** - * Sets listeners to changes of the given option type. - */ + /** Sets listeners to changes of the given option type. */ public void setOptionChangedListener(int option, OptionChangedListener listener) { mOptionChangedListeners.put(option, listener); } - /** - * An interface used to monitor option changes. - */ + /** An interface used to monitor option changes. */ public interface OptionChangedListener { void onOptionChanged(@OptionType int optionType, String newString); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/TvSingletons.java index ac7d4c4a..0c7f78a3 100644 --- a/src/com/android/tv/ApplicationSingletons.java +++ b/src/com/android/tv/TvSingletons.java @@ -16,29 +16,42 @@ package com.android.tv; +import android.content.Context; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; -import com.android.tv.config.RemoteConfig; +import com.android.tv.common.BaseApplication; +import com.android.tv.common.BaseSingletons; +import com.android.tv.common.experiments.ExperimentLoader; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.PreviewDataManager; import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.data.epg.EpgReader; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.util.AccountHelper; +import com.android.tv.tuner.TunerInputController; +import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.account.AccountHelper; +import java.util.concurrent.Executor; +import javax.inject.Provider; -/** - * Interface with getters for application scoped singletons. - */ -public interface ApplicationSingletons { +/** Interface with getters for application scoped singletons. */ +public interface TvSingletons extends BaseSingletons { + + /** Returns the @{@link TvSingletons} using the application context. */ + static TvSingletons getSingletons(Context context) { + return (TvSingletons) BaseApplication.getSingletons(context); + } Analytics getAnalytics(); + void handleInputCountChanged(); + ChannelDataManager getChannelDataManager(); /** @@ -59,8 +72,6 @@ public interface ApplicationSingletons { DvrDataManager getDvrDataManager(); - DvrStorageStatusManager getDvrStorageStatusManager(); - DvrScheduleManager getDvrScheduleManager(); DvrManager getDvrManager(); @@ -73,15 +84,25 @@ public interface ApplicationSingletons { Tracker getTracker(); - TvInputManagerHelper getTvInputManagerHelper(); - MainActivityWrapper getMainActivityWrapper(); AccountHelper getAccountHelper(); - RemoteConfig getRemoteConfig(); - boolean isRunningInMainProcess(); PerformanceMonitor getPerformanceMonitor(); + + TvInputManagerHelper getTvInputManagerHelper(); + + Provider<EpgReader> providesEpgReader(); + + EpgFetcher getEpgFetcher(); + + SetupUtils getSetupUtils(); + + TunerInputController getTunerInputController(); + + ExperimentLoader getExperimentLoader(); + + Executor getDbExecutor(); } diff --git a/src/com/android/tv/analytics/Analytics.java b/src/com/android/tv/analytics/Analytics.java index 27085de7..e0626423 100644 --- a/src/com/android/tv/analytics/Analytics.java +++ b/src/com/android/tv/analytics/Analytics.java @@ -16,21 +16,17 @@ package com.android.tv.analytics; -/** - * Provides Trackers used for user activity analysis. - */ +/** Provides Trackers used for user activity analysis. */ public interface Analytics { Tracker getDefaultTracker(); - /** - * Returns whether the state of the application-level opt is on. - */ + /** Returns whether the state of the application-level opt is on. */ boolean isAppOptOut(); /** - * Sets or resets the application-level opt out flag. If set, no hits will be sent. - * The value of this flag will <i>not</i> persist across application starts. The - * correct value should thus be set in application initialization code. + * Sets or resets the application-level opt out flag. If set, no hits will be sent. The value of + * this flag will <i>not</i> persist across application starts. The correct value should thus be + * set in application initialization code. * * @param optOut {@code true} if application-level opt out should be enforced. */ diff --git a/src/com/android/tv/analytics/ConfigurationInfo.java b/src/com/android/tv/analytics/ConfigurationInfo.java index 41e8baeb..b6bfc5aa 100644 --- a/src/com/android/tv/analytics/ConfigurationInfo.java +++ b/src/com/android/tv/analytics/ConfigurationInfo.java @@ -16,9 +16,7 @@ package com.android.tv.analytics; -/** - * Data useful for tracking that doesn't change often. - */ +/** Data useful for tracking that doesn't change often. */ public class ConfigurationInfo { public final int systemInputCount; public final int nonSystemInputCount; diff --git a/src/com/android/tv/analytics/HasTrackerLabel.java b/src/com/android/tv/analytics/HasTrackerLabel.java index 566e5f1a..04896850 100644 --- a/src/com/android/tv/analytics/HasTrackerLabel.java +++ b/src/com/android/tv/analytics/HasTrackerLabel.java @@ -23,8 +23,6 @@ package com.android.tv.analytics; */ public interface HasTrackerLabel { - /** - * Returns the label. - */ + /** Returns the label. */ String getTrackerLabel(); } diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java index b5b5805c..4a84434c 100644 --- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java +++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java @@ -20,11 +20,9 @@ import android.content.Context; import android.os.Handler; import android.os.Looper; import android.support.annotation.MainThread; - -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.util.RecurringRunner; - import java.util.List; import java.util.concurrent.TimeUnit; @@ -32,53 +30,63 @@ import java.util.concurrent.TimeUnit; * Periodically sends analytics data with the channel count. * * <p> - * <p>This should only be started from a user activity - * like {@link com.android.tv.MainActivity}. + * + * <p>This should only be started from a user activity like {@link com.android.tv.MainActivity}. */ @MainThread public class SendChannelStatusRunnable implements Runnable { private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); - public static RecurringRunner startChannelStatusRecurringRunner(Context context, - Tracker tracker, ChannelDataManager channelDataManager) { + public static RecurringRunner startChannelStatusRecurringRunner( + Context context, Tracker tracker, ChannelDataManager channelDataManager) { - final SendChannelStatusRunnable sendChannelStatusRunnable = new SendChannelStatusRunnable( - channelDataManager, tracker); + final SendChannelStatusRunnable sendChannelStatusRunnable = + new SendChannelStatusRunnable(channelDataManager, tracker); - Runnable onStopRunnable = new Runnable() { - @Override - public void run() { - sendChannelStatusRunnable.setDbLoadListener(null); - } - }; - final RecurringRunner recurringRunner = new RecurringRunner(context, - SEND_CHANNEL_STATUS_INTERVAL_MS, sendChannelStatusRunnable, onStopRunnable); + Runnable onStopRunnable = + new Runnable() { + @Override + public void run() { + sendChannelStatusRunnable.setDbLoadListener(null); + } + }; + final RecurringRunner recurringRunner = + new RecurringRunner( + context, + SEND_CHANNEL_STATUS_INTERVAL_MS, + sendChannelStatusRunnable, + onStopRunnable); if (channelDataManager.isDbLoadFinished()) { sendChannelStatusRunnable.setDbLoadListener(null); recurringRunner.start(); } else { - //Start the recurring runnable after the channel DB is finished loading. - sendChannelStatusRunnable.setDbLoadListener(new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - // This is called inside an iterator of Listeners so the remove step is done - // via a post on the main thread - new Handler(Looper.getMainLooper()).post(new Runnable() { + // Start the recurring runnable after the channel DB is finished loading. + sendChannelStatusRunnable.setDbLoadListener( + new ChannelDataManager.Listener() { @Override - public void run() { - sendChannelStatusRunnable.setDbLoadListener(null); + public void onLoadFinished() { + // This is called inside an iterator of Listeners so the remove step is + // done + // via a post on the main thread + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + sendChannelStatusRunnable.setDbLoadListener( + null); + } + }); + recurringRunner.start(); } - }); - recurringRunner.start(); - } - @Override - public void onChannelListUpdated() { } + @Override + public void onChannelListUpdated() {} - @Override - public void onChannelBrowsableChanged() { } - }); + @Override + public void onChannelBrowsableChanged() {} + }); } return recurringRunner; } diff --git a/src/com/android/tv/analytics/SendConfigInfoRunnable.java b/src/com/android/tv/analytics/SendConfigInfoRunnable.java index 41392a6d..d4674086 100644 --- a/src/com/android/tv/analytics/SendConfigInfoRunnable.java +++ b/src/com/android/tv/analytics/SendConfigInfoRunnable.java @@ -17,14 +17,10 @@ package com.android.tv.analytics; import android.media.tv.TvInputInfo; - import com.android.tv.util.TvInputManagerHelper; - import java.util.List; -/** - * Sends ConfigurationInfo once a day. - */ +/** Sends ConfigurationInfo once a day. */ public class SendConfigInfoRunnable implements Runnable { private final Tracker mTracker; private final TvInputManagerHelper mTvInputManagerHelper; @@ -46,8 +42,8 @@ public class SendConfigInfoRunnable implements Runnable { nonSystemInputCount++; } } - ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount, - nonSystemInputCount); + ConfigurationInfo configurationInfo = + new ConfigurationInfo(systemInputCount, nonSystemInputCount); mTracker.sendConfigurationInfo(configurationInfo); } } diff --git a/src/com/android/tv/analytics/StubAnalytics.java b/src/com/android/tv/analytics/StubAnalytics.java index 99c10d94..2c58b9b8 100644 --- a/src/com/android/tv/analytics/StubAnalytics.java +++ b/src/com/android/tv/analytics/StubAnalytics.java @@ -19,9 +19,7 @@ package com.android.tv.analytics; import android.app.Application; import android.content.Context; -/** - * An implementation of {@link Analytics} that returns a {@link StubTracker}. - */ +/** An implementation of {@link Analytics} that returns a {@link StubTracker}. */ public final class StubAnalytics implements Analytics { public static StubAnalytics getInstance(Application application) { return new StubAnalytics(application); @@ -30,8 +28,7 @@ public final class StubAnalytics implements Analytics { private final Tracker mTracker = new StubTracker(); private boolean mOptOut = true; - private StubAnalytics(Context context) { - } + private StubAnalytics(Context context) {} @Override public Tracker getDefaultTracker() { diff --git a/src/com/android/tv/analytics/StubTracker.java b/src/com/android/tv/analytics/StubTracker.java index 6e64ebca..e11b91c2 100644 --- a/src/com/android/tv/analytics/StubTracker.java +++ b/src/com/android/tv/analytics/StubTracker.java @@ -17,111 +17,108 @@ package com.android.tv.analytics; import android.support.annotation.VisibleForTesting; - import com.android.tv.TimeShiftManager; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; -/** - * A implementation of Tracker that does nothing. - */ +/** A implementation of Tracker that does nothing. */ @VisibleForTesting public class StubTracker implements Tracker { @Override - public void sendChannelCount(int browsableChannelCount, int totalChannelCount) { } + public void sendChannelCount(int browsableChannelCount, int totalChannelCount) {} @Override - public void sendConfigurationInfo(ConfigurationInfo info) { } + public void sendConfigurationInfo(ConfigurationInfo info) {} @Override - public void sendMainStart() { } + public void sendMainStart() {} @Override - public void sendMainStop(long durationMs) { } + public void sendMainStop(long durationMs) {} @Override - public void sendScreenView(String screenName) { } + public void sendScreenView(String screenName) {} @Override - public void sendChannelViewStart(Channel channel, boolean tunedByRecommendation) { } + public void sendChannelViewStart(Channel channel, boolean tunedByRecommendation) {} @Override - public void sendChannelTuneTime(Channel channel, long durationMs) { } + public void sendChannelTuneTime(Channel channel, long durationMs) {} @Override - public void sendChannelViewStop(Channel channel, long durationMs) { } + public void sendChannelViewStop(Channel channel, long durationMs) {} @Override - public void sendChannelUp() { } + public void sendChannelUp() {} @Override - public void sendChannelDown() { } + public void sendChannelDown() {} @Override - public void sendShowMenu() { } + public void sendShowMenu() {} @Override - public void sendHideMenu(long durationMs) { } + public void sendHideMenu(long durationMs) {} @Override - public void sendMenuClicked(String label) { } + public void sendMenuClicked(String label) {} @Override - public void sendMenuClicked(int labelResId) { } + public void sendMenuClicked(int labelResId) {} @Override - public void sendShowEpg() { } + public void sendShowEpg() {} @Override - public void sendEpgItemClicked() { } + public void sendEpgItemClicked() {} @Override - public void sendHideEpg(long durationMs) { } + public void sendHideEpg(long durationMs) {} @Override - public void sendShowChannelSwitch() { } + public void sendShowChannelSwitch() {} @Override - public void sendHideChannelSwitch(long durationMs) { } + public void sendHideChannelSwitch(long durationMs) {} @Override - public void sendChannelNumberInput() { } + public void sendChannelNumberInput() {} @Override - public void sendChannelInputNavigated() { } + public void sendChannelInputNavigated() {} @Override - public void sendChannelNumberItemClicked() { } + public void sendChannelNumberItemClicked() {} @Override - public void sendChannelNumberItemChosenByTimeout() { } + public void sendChannelNumberItemChosenByTimeout() {} @Override - public void sendChannelVideoUnavailable(Channel channel, int reason) { } + public void sendChannelVideoUnavailable(Channel channel, int reason) {} @Override - public void sendAc3PassthroughCapabilities(boolean isSupported) { } + public void sendAc3PassthroughCapabilities(boolean isSupported) {} @Override - public void sendInputConnectionFailure(String inputId) { } + public void sendInputConnectionFailure(String inputId) {} @Override - public void sendInputDisconnected(String inputId) { } + public void sendInputDisconnected(String inputId) {} @Override - public void sendShowInputSelection() { } + public void sendShowInputSelection() {} @Override - public void sendHideInputSelection(long durationMs) { } + public void sendHideInputSelection(long durationMs) {} @Override - public void sendInputSelected(String inputLabel) { } + public void sendInputSelected(String inputLabel) {} @Override - public void sendShowSidePanel(HasTrackerLabel trackerLabel) { } + public void sendShowSidePanel(HasTrackerLabel trackerLabel) {} @Override - public void sendHideSidePanel(HasTrackerLabel trackerLabel, long durationMs) { } + public void sendHideSidePanel(HasTrackerLabel trackerLabel, long durationMs) {} @Override - public void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId) { } + public void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId) {} } diff --git a/src/com/android/tv/analytics/Tracker.java b/src/com/android/tv/analytics/Tracker.java index 291fc9ce..0fcef5dc 100644 --- a/src/com/android/tv/analytics/Tracker.java +++ b/src/com/android/tv/analytics/Tracker.java @@ -17,11 +17,9 @@ package com.android.tv.analytics; import com.android.tv.TimeShiftManager; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; -/** - * Interface for sending user activity for analysis. - */ +/** Interface for sending user activity for analysis. */ public interface Tracker { /** @@ -45,9 +43,7 @@ public interface Tracker { */ void sendConfigurationInfo(ConfigurationInfo info); - /** - * Sends tracking information for starting the MainActivity. - */ + /** Sends tracking information for starting the MainActivity. */ void sendMainStart(); /** @@ -55,11 +51,9 @@ public interface Tracker { * * @param durationMs The time main activity was "started" in milliseconds. */ - void sendMainStop( long durationMs); + void sendMainStop(long durationMs); - /** - * Sets the screen name and sends a ScreenView hit. - */ + /** Sets the screen name and sends a ScreenView hit. */ void sendScreenView(String screenName); /** @@ -86,19 +80,13 @@ public interface Tracker { */ void sendChannelViewStop(Channel channel, long durationMs); - /** - * Sends tracking information for pressing channel up. - */ + /** Sends tracking information for pressing channel up. */ void sendChannelUp(); - /** - * Sends tracking information for pressing channel down. - */ + /** Sends tracking information for pressing channel down. */ void sendChannelDown(); - /** - * Sends tracking information for showing the main menu. - */ + /** Sends tracking information for showing the main menu. */ void sendShowMenu(); /** @@ -126,14 +114,10 @@ public interface Tracker { */ void sendMenuClicked(int labelResId); - /** - * Sends tracking information for showing the Electronic Program Guide (EPG). - */ + /** Sends tracking information for showing the Electronic Program Guide (EPG). */ void sendShowEpg(); - /** - * Sends tracking information for clicking an Electronic Program Guide (EPG) item. - */ + /** Sends tracking information for clicking an Electronic Program Guide (EPG) item. */ void sendEpgItemClicked(); /** @@ -143,9 +127,7 @@ public interface Tracker { */ void sendHideEpg(long durationMs); - /** - * Sends tracking information for showing the channel switch view. - */ + /** Sends tracking information for showing the channel switch view. */ void sendShowChannelSwitch(); /** @@ -155,9 +137,7 @@ public interface Tracker { */ void sendHideChannelSwitch(long durationMs); - /** - * Sends tracking for each channel number or delimiter pressed. - */ + /** Sends tracking for each channel number or delimiter pressed. */ void sendChannelNumberInput(); /** @@ -167,19 +147,13 @@ public interface Tracker { */ void sendChannelInputNavigated(); - /** - * Sends tracking for channel clicked. - */ + /** Sends tracking for channel clicked. */ void sendChannelNumberItemClicked(); - /** - * Sends tracking for channel chosen (tuned) because the channel switch view timed out. - */ + /** Sends tracking for channel chosen (tuned) because the channel switch view timed out. */ void sendChannelNumberItemChosenByTimeout(); - /** - * Sends tracking for the reason video is unavailable on a channel. - */ + /** Sends tracking for the reason video is unavailable on a channel. */ void sendChannelVideoUnavailable(Channel channel, int reason); /** @@ -191,6 +165,7 @@ public interface Tracker { /** * Sends tracking for input a connection failure. + * * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId. * * @param inputId the input the failure happened on @@ -199,15 +174,14 @@ public interface Tracker { /** * Sends tracking for input disconnected. + * * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId. * * @param inputId the input the failure happened on */ void sendInputDisconnected(String inputId); - /** - * Sends tracking information for showing the input selection view. - */ + /** Sends tracking information for showing the input selection view. */ void sendShowInputSelection(); /** diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java new file mode 100644 index 00000000..461331d5 --- /dev/null +++ b/src/com/android/tv/app/LiveTvApplication.java @@ -0,0 +1,139 @@ +/* + * 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.app; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvContract; +import com.android.tv.TvApplication; +import com.android.tv.analytics.Analytics; +import com.android.tv.analytics.StubAnalytics; +import com.android.tv.analytics.Tracker; +import com.android.tv.common.CommonConstants; +import com.android.tv.common.actions.InputSetupActionUtils; +import com.android.tv.common.config.DefaultConfigManager; +import com.android.tv.common.config.api.RemoteConfig; +import com.android.tv.common.experiments.ExperimentLoader; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.data.epg.EpgReader; +import com.android.tv.data.epg.StubEpgReader; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.StubPerformanceMonitor; +import com.android.tv.tuner.livetuner.LiveTvTunerTvInputService; +import com.android.tv.tuner.setup.LiveTvTunerSetupActivity; +import com.android.tv.util.account.AccountHelper; +import com.android.tv.util.account.AccountHelperImpl; +import javax.inject.Provider; + +/** The top level application for Live TV. */ +public class LiveTvApplication extends TvApplication { + protected static final String TV_ACTIVITY_CLASS_NAME = + CommonConstants.BASE_PACKAGE + ".TvActivity"; + + private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor(); + private final Provider<EpgReader> mEpgReaderProvider = + new Provider<EpgReader>() { + + @Override + public EpgReader get() { + return new StubEpgReader(LiveTvApplication.this); + } + }; + + private AccountHelper mAccountHelper; + private Analytics mAnalytics; + private Tracker mTracker; + private String mEmbeddedInputId; + private RemoteConfig mRemoteConfig; + private ExperimentLoader mExperimentLoader; + + /** Returns the {@link AccountHelperImpl}. */ + @Override + public AccountHelper getAccountHelper() { + if (mAccountHelper == null) { + mAccountHelper = new AccountHelperImpl(getApplicationContext()); + } + return mAccountHelper; + } + + @Override + public synchronized PerformanceMonitor getPerformanceMonitor() { + return performanceMonitor; + } + + @Override + public Provider<EpgReader> providesEpgReader() { + return mEpgReaderProvider; + } + + @Override + public ExperimentLoader getExperimentLoader() { + mExperimentLoader = new ExperimentLoader(); + return mExperimentLoader; + } + + /** Returns the {@link Analytics}. */ + @Override + public synchronized Analytics getAnalytics() { + if (mAnalytics == null) { + mAnalytics = StubAnalytics.getInstance(this); + } + return mAnalytics; + } + + /** Returns the default tracker. */ + @Override + public synchronized Tracker getTracker() { + if (mTracker == null) { + mTracker = getAnalytics().getDefaultTracker(); + } + return mTracker; + } + + @Override + public Intent getTunerSetupIntent(Context context) { + // Make an intent to launch the setup activity of TV tuner input. + Intent intent = + CommonUtils.createSetupIntent( + new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId); + intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId); + Intent tvActivityIntent = new Intent(); + tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); + intent.putExtra(InputSetupActionUtils.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); + return intent; + } + + @Override + public synchronized String getEmbeddedTunerInputId() { + if (mEmbeddedInputId == null) { + mEmbeddedInputId = + TvContract.buildInputId( + new ComponentName(this, LiveTvTunerTvInputService.class)); + } + return mEmbeddedInputId; + } + + @Override + public RemoteConfig getRemoteConfig() { + if (mRemoteConfig == null) { + // No need to synchronize this, it does not hurt to create two and throw one away. + mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig(); + } + return mRemoteConfig; + } +} diff --git a/src/com/android/tv/config/DefaultConfigManager.java b/src/com/android/tv/config/DefaultConfigManager.java deleted file mode 100644 index bbabc6d4..00000000 --- a/src/com/android/tv/config/DefaultConfigManager.java +++ /dev/null @@ -1,61 +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.config; - -import android.content.Context; - -/** - * Stub Remote Config. - */ -public class DefaultConfigManager { - public static final long DEFAULT_LONG_VALUE = 0; - public static DefaultConfigManager createInstance(Context context) { - return new DefaultConfigManager(); - } - - private StubRemoteConfig mRemoteConfig = new StubRemoteConfig(); - - public RemoteConfig getRemoteConfig() { - return mRemoteConfig; - } - - private static class StubRemoteConfig implements RemoteConfig { - @Override - public void fetch(OnRemoteConfigUpdatedListener listener) { - - } - - @Override - public String getString(String key) { - return null; - } - - @Override - public boolean getBoolean(String key) { - return false; - } - - @Override - public long getLong(String key) { - return DEFAULT_LONG_VALUE; - } - } -} - - - - diff --git a/src/com/android/tv/config/RemoteConfig.java b/src/com/android/tv/config/RemoteConfig.java deleted file mode 100644 index f7ae87e7..00000000 --- a/src/com/android/tv/config/RemoteConfig.java +++ /dev/null @@ -1,51 +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.config; - -/** - * Manages Live TV Configuration, allowing remote updates. - * - * <p>This is a thin wrapper around - * <a href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a> - */ -public interface RemoteConfig { - - /** - * Notified on successful completion of a {@link #fetch)} - */ - interface OnRemoteConfigUpdatedListener { - void onRemoteConfigUpdated(); - } - - /** - * Starts a fetch and notifies {@code listener} on successful completion. - */ - void fetch(OnRemoteConfigUpdatedListener listener); - - /** - * Gets value as a string corresponding to the specified key. - */ - String getString(String key); - - /** - * Gets value as a boolean corresponding to the specified key. - */ - boolean getBoolean(String key); - - /** Gets value as a long corresponding to the specified key. */ - long getLong(String key); -} diff --git a/src/com/android/tv/config/RemoteConfigFeature.java b/src/com/android/tv/config/RemoteConfigFeature.java deleted file mode 100644 index 502e6a9c..00000000 --- a/src/com/android/tv/config/RemoteConfigFeature.java +++ /dev/null @@ -1,43 +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.config; - -import android.content.Context; - -import com.android.tv.TvApplication; -import com.android.tv.common.feature.Feature; - -/** - * A {@link Feature} controlled by a {@link RemoteConfig} boolean. - */ -public class RemoteConfigFeature implements Feature { - private final String mKey; - - /** Creates a {@link RemoteConfigFeature for the {@code key}. */ - public static RemoteConfigFeature fromKey(String key) { - return new RemoteConfigFeature(key); - } - - private RemoteConfigFeature(String key) { - mKey = key; - } - - @Override - public boolean isEnabled(Context context) { - return TvApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey); - } -} diff --git a/src/com/android/tv/config/RemoteConfigUtils.java b/src/com/android/tv/config/RemoteConfigUtils.java deleted file mode 100644 index 09d85239..00000000 --- a/src/com/android/tv/config/RemoteConfigUtils.java +++ /dev/null @@ -1,42 +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.config; - -import android.content.Context; -import android.util.Log; -import com.android.tv.TvApplication; - -/** A utility class to get the remote config. */ -public class RemoteConfigUtils { - private static final String TAG = "RemoteConfigUtils"; - private static final boolean DEBUG = false; - - private RemoteConfigUtils() {} - - public static long getRemoteConfig(Context context, String key, long defaultValue) { - RemoteConfig remoteConfig = TvApplication.getSingletons(context).getRemoteConfig(); - try { - long remoteValue = remoteConfig.getLong(key); - if (DEBUG) Log.d(TAG, "Got " + key + " from remote: " + remoteValue); - return remoteValue; - } catch (Exception e) { - Log.w(TAG, "Cannot get " + key + " from RemoteConfig", e); - } - if (DEBUG) Log.d(TAG, "Use default value " + defaultValue); - return defaultValue; - } -} diff --git a/src/com/android/tv/customization/CustomAction.java b/src/com/android/tv/customization/CustomAction.java deleted file mode 100644 index b8f4695b..00000000 --- a/src/com/android/tv/customization/CustomAction.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.customization; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; - -/** - * Describes a custom option defined in customization package. - * This will be added to main menu. - */ -public class CustomAction implements Comparable<CustomAction> { - private static final int POSITION_THRESHOLD = 100; - - private final int mPositionPriority; - private final String mTitle; - private final Drawable mIconDrawable; - private final Intent mIntent; - - public CustomAction(int positionPriority, String title, Drawable iconDrawable, Intent intent) { - mPositionPriority = positionPriority; - mTitle = title; - mIconDrawable = iconDrawable; - mIntent = intent; - } - - /** - * Returns if this option comes before the existing items. - * Note that custom options can only be placed at the front or back. - * (i.e. cannot be added in the middle of existing options.) - * @return {@code true} if it goes to the beginning. {@code false} if it goes to the end. - */ - public boolean isFront() { - return mPositionPriority < POSITION_THRESHOLD; - } - - @Override - public int compareTo(@NonNull CustomAction another) { - return mPositionPriority - another.mPositionPriority; - } - - /** - * Returns title. - */ - public String getTitle() { - return mTitle; - } - - /** - * Returns icon drawable. - */ - public Drawable getIconDrawable() { - return mIconDrawable; - } - - /** - * Returns intent to launch when this option is clicked. - */ - public Intent getIntent() { - return mIntent; - } -} diff --git a/src/com/android/tv/customization/TvCustomizationManager.java b/src/com/android/tv/customization/TvCustomizationManager.java deleted file mode 100644 index ed6b98ca..00000000 --- a/src/com/android/tv/customization/TvCustomizationManager.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.customization; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.support.annotation.IntDef; -import android.text.TextUtils; -import android.util.Log; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class TvCustomizationManager { - private static final String TAG = "TvCustomizationManager"; - private static final boolean DEBUG = false; - - private static final String[] CUSTOMIZE_PERMISSIONS = { - "com.android.tv.permission.CUSTOMIZE_TV_APP" - }; - - private static final String CATEGORY_TV_CUSTOMIZATION = - "com.android.tv.category"; - - /** - * Row IDs to share customized actions. - * Only rows listed below can have customized action. - */ - public static final String ID_OPTIONS_ROW = "options_row"; - public static final String ID_PARTNER_ROW = "partner_row"; - - @IntDef({TRICKPLAY_MODE_ENABLED, TRICKPLAY_MODE_DISABLED, TRICKPLAY_MODE_USE_EXTERNAL_STORAGE}) - @Retention(RetentionPolicy.SOURCE) - public @interface TRICKPLAY_MODE {} - public static final int TRICKPLAY_MODE_ENABLED = 0; - public static final int TRICKPLAY_MODE_DISABLED = 1; - public static final int TRICKPLAY_MODE_USE_EXTERNAL_STORAGE = 2; - - private static final String[] TRICKPLAY_MODE_STRINGS = { - "enabled", - "disabled", - "use_external_storage_only" - }; - - private static final HashMap<String, String> INTENT_CATEGORY_TO_ROW_ID; - static { - INTENT_CATEGORY_TO_ROW_ID = new HashMap<>(); - INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".OPTIONS_ROW", ID_OPTIONS_ROW); - INTENT_CATEGORY_TO_ROW_ID.put(CATEGORY_TV_CUSTOMIZATION + ".PARTNER_ROW", ID_PARTNER_ROW); - } - - private static final String RES_ID_PARTNER_ROW_TITLE = "partner_row_title"; - private static final String RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER = - "has_linux_dvb_built_in_tuner"; - private static final String RES_ID_TRICKPLAY_MODE = "trickplay_mode"; - - private static final String RES_TYPE_STRING = "string"; - private static final String RES_TYPE_BOOLEAN = "bool"; - - private static String sCustomizationPackage; - private static Boolean sHasLinuxDvbBuiltInTuner; - private static @TRICKPLAY_MODE Integer sTrickplayMode; - - private final Context mContext; - private boolean mInitialized; - - private String mPartnerRowTitle; - private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>(); - - public TvCustomizationManager(Context context) { - mContext = context; - mInitialized = false; - } - - /** - * Returns {@code true} if there's a customization package installed and it specifies built-in - * tuner devices are available. The built-in tuner should support DVB API to be recognized by - * Live TV. - */ - public static boolean hasLinuxDvbBuiltInTuner(Context context) { - if (sHasLinuxDvbBuiltInTuner == null) { - if (TextUtils.isEmpty(getCustomizationPackageName(context))) { - sHasLinuxDvbBuiltInTuner = false; - } else { - try { - Resources res = context.getPackageManager() - .getResourcesForApplication(sCustomizationPackage); - int resId = res.getIdentifier(RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER, - RES_TYPE_BOOLEAN, sCustomizationPackage); - sHasLinuxDvbBuiltInTuner = resId != 0 && res.getBoolean(resId); - } catch (NameNotFoundException e) { - sHasLinuxDvbBuiltInTuner = false; - } - } - } - return sHasLinuxDvbBuiltInTuner; - } - - public static @TRICKPLAY_MODE int getTrickplayMode(Context context) { - if (sTrickplayMode == null) { - if (TextUtils.isEmpty(getCustomizationPackageName(context))) { - sTrickplayMode = TRICKPLAY_MODE_ENABLED; - } else { - try { - String customization = null; - Resources res = context.getPackageManager() - .getResourcesForApplication(sCustomizationPackage); - int resId = res.getIdentifier(RES_ID_TRICKPLAY_MODE, - RES_TYPE_STRING, sCustomizationPackage); - customization = resId == 0 ? null : res.getString(resId); - sTrickplayMode = TRICKPLAY_MODE_ENABLED; - if (customization != null) { - for (int i = 0; i < TRICKPLAY_MODE_STRINGS.length; ++i) { - if (TRICKPLAY_MODE_STRINGS[i].equalsIgnoreCase(customization)) { - sTrickplayMode = i; - break; - } - } - } - } catch (NameNotFoundException e) { - sTrickplayMode = TRICKPLAY_MODE_ENABLED; - } - } - } - return sTrickplayMode; - } - - private static String getCustomizationPackageName(Context context) { - if (sCustomizationPackage == null) { - List<PackageInfo> packageInfos = context.getPackageManager() - .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0); - sCustomizationPackage = packageInfos.size() == 0 ? "" : packageInfos.get(0).packageName; - } - return sCustomizationPackage; - } - - /** - * Initialize TV customization options. - * Run this API only on the main thread. - */ - public void initialize() { - if (mInitialized) { - return; - } - mInitialized = true; - if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) { - buildCustomActions(); - buildPartnerRow(); - } - } - - private void buildCustomActions() { - mRowIdToCustomActionsMap.clear(); - PackageManager pm = mContext.getPackageManager(); - for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) { - Intent customOptionIntent = new Intent(Intent.ACTION_MAIN); - customOptionIntent.addCategory(intentCategory); - - List<ResolveInfo> activities = pm.queryIntentActivities(customOptionIntent, - PackageManager.GET_RECEIVERS | PackageManager.GET_RESOLVED_FILTER - | PackageManager.GET_META_DATA); - for (ResolveInfo info : activities) { - String packageName = info.activityInfo.packageName; - if (!TextUtils.equals(packageName, sCustomizationPackage)) { - Log.w(TAG, "A customization package " + sCustomizationPackage - + " already exist. Ignoring " + packageName); - continue; - } - - int position = info.filter.getPriority(); - String title = info.loadLabel(pm).toString(); - Drawable drawable = info.loadIcon(pm); - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(intentCategory); - intent.setClassName(sCustomizationPackage, info.activityInfo.name); - - String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory); - List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId); - if (actions == null) { - actions = new ArrayList<>(); - mRowIdToCustomActionsMap.put(rowId, actions); - } - actions.add(new CustomAction(position, title, drawable, intent)); - } - } - // Sort items by position - for (List<CustomAction> actions : mRowIdToCustomActionsMap.values()) { - Collections.sort(actions); - } - - if (DEBUG) { - Log.d(TAG, "Dumping custom actions"); - for (String id : mRowIdToCustomActionsMap.keySet()) { - for (CustomAction action : mRowIdToCustomActionsMap.get(id)) { - Log.d(TAG, "Custom row rowId=" + id + " title=" + action.getTitle() - + " class=" + action.getIntent()); - } - } - Log.d(TAG, "Dumping custom actions - end of dump"); - } - } - - /** - * Returns custom actions for given row id. - * - * Row ID is one of ID_OPTIONS_ROW or ID_PARTNER_ROW. - */ - public List<CustomAction> getCustomActions(String rowId) { - return mRowIdToCustomActionsMap.get(rowId); - } - - private void buildPartnerRow() { - mPartnerRowTitle = null; - Resources res; - try { - res = mContext.getPackageManager() - .getResourcesForApplication(sCustomizationPackage); - } catch (NameNotFoundException e) { - Log.w(TAG, "Could not get resources for package " + sCustomizationPackage); - return; - } - int resId = res.getIdentifier( - RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage); - if (resId != 0) { - mPartnerRowTitle = res.getString(resId); - } - if (DEBUG) Log.d(TAG, "Partner row title [" + mPartnerRowTitle + "]"); - } - - /** - * Returns partner row title. - */ - public String getPartnerRowTitle() { - return mPartnerRowTitle; - } -} diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java index 4e36c80a..0fb1e58d 100644 --- a/src/com/android/tv/data/BaseProgram.java +++ b/src/com/android/tv/data/BaseProgram.java @@ -20,14 +20,12 @@ import android.content.Context; import android.media.tv.TvContentRating; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.R; - import java.util.Comparator; /** - * Base class for {@link com.android.tv.data.Program} and - * {@link com.android.tv.dvr.data.RecordedProgram}. + * Base class for {@link com.android.tv.data.Program} and {@link + * com.android.tv.dvr.data.RecordedProgram}. */ public abstract class BaseProgram { /** @@ -35,8 +33,7 @@ public abstract class BaseProgram { * If a program's season or episode number is null, it will be consider "smaller" than programs * with season or episode numbers. */ - public static final Comparator<BaseProgram> EPISODE_COMPARATOR = - new EpisodeComparator(false); + public static final Comparator<BaseProgram> EPISODE_COMPARATOR = new EpisodeComparator(false); /** * Comparator used to compare {@link BaseProgram} according to its season and episodes number @@ -58,8 +55,7 @@ public abstract class BaseProgram { if (lhs == rhs) { return 0; } - int seasonNumberCompare = - numberCompare(lhs.getSeasonNumber(), rhs.getSeasonNumber()); + int seasonNumberCompare = numberCompare(lhs.getSeasonNumber(), rhs.getSeasonNumber()); if (seasonNumberCompare != 0) { return mReversedSeason ? -seasonNumberCompare : seasonNumberCompare; } else { @@ -68,9 +64,7 @@ public abstract class BaseProgram { } } - /** - * Compares two strings represent season numbers or episode numbers of programs. - */ + /** Compares two strings represent season numbers or episode numbers of programs. */ public static int numberCompare(String s1, String s2) { if (s1 == s2) { return 0; @@ -88,121 +82,120 @@ public abstract class BaseProgram { } } - /** - * Returns ID of the program. - */ - abstract public long getId(); + /** Returns ID of the program. */ + public abstract long getId(); - /** - * Returns the title of the program. - */ - abstract public String getTitle(); + /** Returns the title of the program. */ + public abstract String getTitle(); - /** - * Returns the episode title. - */ - abstract public String getEpisodeTitle(); + /** Returns the episode title. */ + public abstract String getEpisodeTitle(); - /** - * Returns the displayed title of the program episode. - */ + /** Returns the displayed title of the program episode. */ public String getEpisodeDisplayTitle(Context context) { - if (!TextUtils.isEmpty(getEpisodeNumber())) { - String episodeTitle = getEpisodeTitle() == null ? "" : getEpisodeTitle(); - if (TextUtils.equals(getSeasonNumber(), "0")) { + String episodeNumber = getEpisodeNumber(); + String episodeTitle = getEpisodeTitle(); + if (!TextUtils.isEmpty(episodeNumber)) { + episodeTitle = episodeTitle == null ? "" : episodeTitle; + String seasonNumber = getSeasonNumber(); + if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) { // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_title_format_no_season_number), - getEpisodeNumber(), episodeTitle); + return context.getResources() + .getString( + R.string.display_episode_title_format_no_season_number, + episodeNumber, + episodeTitle); } else { - return String.format(context.getResources().getString( - R.string.display_episode_title_format), - getSeasonNumber(), getEpisodeNumber(), episodeTitle); + return context.getResources() + .getString( + R.string.display_episode_title_format, + seasonNumber, + episodeNumber, + episodeTitle); } } - return getEpisodeTitle(); + return episodeTitle; } /** - * Returns the description of the program. - */ - abstract public String getDescription(); + * Returns the content description of the program episode, suitable for being spoken by an + * accessibility service. + */ + public String getEpisodeContentDescription(Context context) { + String episodeNumber = getEpisodeNumber(); + String episodeTitle = getEpisodeTitle(); + if (!TextUtils.isEmpty(episodeNumber)) { + episodeTitle = episodeTitle == null ? "" : episodeTitle; + String seasonNumber = getSeasonNumber(); + if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) { + // Do not list season if it is empty or 0 + return context.getResources() + .getString( + R.string.content_description_episode_format_no_season_number, + episodeNumber, + episodeTitle); + } else { + return context.getResources() + .getString( + R.string.content_description_episode_format, + seasonNumber, + episodeNumber, + episodeTitle); + } + } + return episodeTitle; + } - /** - * Returns the long description of the program. - */ - abstract public String getLongDescription(); + /** Returns the description of the program. */ + public abstract String getDescription(); - /** - * Returns the start time of the program in Milliseconds. - */ - abstract public long getStartTimeUtcMillis(); + /** Returns the long description of the program. */ + public abstract String getLongDescription(); - /** - * Returns the end time of the program in Milliseconds. - */ - abstract public long getEndTimeUtcMillis(); + /** Returns the start time of the program in Milliseconds. */ + public abstract long getStartTimeUtcMillis(); - /** - * Returns the duration of the program in Milliseconds. - */ - abstract public long getDurationMillis(); + /** Returns the end time of the program in Milliseconds. */ + public abstract long getEndTimeUtcMillis(); - /** - * Returns the series ID. - */ - abstract public String getSeriesId(); + /** Returns the duration of the program in Milliseconds. */ + public abstract long getDurationMillis(); - /** - * Returns the season number. - */ - abstract public String getSeasonNumber(); + /** Returns the series ID. */ + public abstract String getSeriesId(); - /** - * Returns the episode number. - */ - abstract public String getEpisodeNumber(); + /** Returns the season number. */ + public abstract String getSeasonNumber(); - /** - * Returns URI of the program's poster. - */ - abstract public String getPosterArtUri(); + /** Returns the episode number. */ + public abstract String getEpisodeNumber(); - /** - * Returns URI of the program's thumbnail. - */ - abstract public String getThumbnailUri(); + /** Returns URI of the program's poster. */ + public abstract String getPosterArtUri(); - /** - * Returns the array of the ID's of the canonical genres. - */ - abstract public int[] getCanonicalGenreIds(); + /** Returns URI of the program's thumbnail. */ + public abstract String getThumbnailUri(); + + /** Returns the array of the ID's of the canonical genres. */ + public abstract int[] getCanonicalGenreIds(); /** Returns the array of content ratings. */ @Nullable - abstract public TvContentRating[] getContentRatings(); + public abstract TvContentRating[] getContentRatings(); - /** - * Returns channel's ID of the program. - */ - abstract public long getChannelId(); + /** Returns channel's ID of the program. */ + public abstract long getChannelId(); - /** - * Returns if the program is valid. - */ - abstract public boolean isValid(); + /** Returns if the program is valid. */ + public abstract boolean isValid(); - /** - * Checks whether the program is episodic or not. - */ + /** Checks whether the program is episodic or not. */ public boolean isEpisodic() { return getSeriesId() != null; } - /** - * Generates the series ID for the other inputs than the tuner TV input. - */ + /** Generates the series ID for the other inputs than the tuner TV input. */ public static String generateSeriesId(String packageName, String title) { return packageName + "/" + title; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 6f93fbd1..1dfcf125 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -38,15 +38,15 @@ import android.support.annotation.VisibleForTesting; import android.util.ArraySet; import android.util.Log; import android.util.MutableInt; - -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.common.util.SharedPreferencesUtils; +import com.android.tv.data.api.Channel; import com.android.tv.util.AsyncDbTask; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -56,13 +56,13 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executor; /** - * The class to manage channel data. - * Basic features: reading channel list and each channel's current program, and updating - * the values of {@link Channels#COLUMN_BROWSABLE}, {@link Channels#COLUMN_LOCKED}. - * This class is not thread-safe and under an assumption that its public methods are called in - * only the main thread. + * The class to manage channel data. Basic features: reading channel list and each channel's current + * program, and updating the values of {@link Channels#COLUMN_BROWSABLE}, {@link + * Channels#COLUMN_LOCKED}. This class is not thread-safe and under an assumption that its public + * methods are called in only the main thread. */ @AnyThread public class ChannelDataManager { @@ -73,6 +73,7 @@ public class ChannelDataManager { private final Context mContext; private final TvInputManagerHelper mInputManager; + private final Executor mDbExecutor; private boolean mStarted; private boolean mDbLoadFinished; private QueryAllChannelsTask mChannelsUpdateTask; @@ -81,8 +82,8 @@ public class ChannelDataManager { private final Set<Listener> mListeners = new CopyOnWriteArraySet<>(); // Use container class to support multi-thread safety. This value can be set only on the main // thread. - volatile private UnmodifiableChannelData mData = new UnmodifiableChannelData(); - private final Channel.DefaultComparator mChannelComparator; + private volatile UnmodifiableChannelData mData = new UnmodifiableChannelData(); + private final ChannelImpl.DefaultComparator mChannelComparator; private final Handler mHandler; private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>(); @@ -93,81 +94,92 @@ public class ChannelDataManager { private final boolean mStoreBrowsableInSharedPreferences; private final SharedPreferences mBrowsableSharedPreferences; - private final TvInputCallback mTvInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - boolean channelAdded = false; - ChannelData data = new ChannelData(mData); - for (ChannelWrapper channel : mData.channelWrapperMap.values()) { - if (channel.mChannel.getInputId().equals(inputId)) { - channel.mInputRemoved = false; - addChannel(data, channel.mChannel); - channelAdded = true; + private final TvInputCallback mTvInputCallback = + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + boolean channelAdded = false; + ChannelData data = new ChannelData(mData); + for (ChannelWrapper channel : mData.channelWrapperMap.values()) { + if (channel.mChannel.getInputId().equals(inputId)) { + channel.mInputRemoved = false; + addChannel(data, channel.mChannel); + channelAdded = true; + } + } + if (channelAdded) { + Collections.sort(data.channels, mChannelComparator); + mData = new UnmodifiableChannelData(data); + notifyChannelListUpdated(); + } } - } - if (channelAdded) { - Collections.sort(data.channels, mChannelComparator); - mData = new UnmodifiableChannelData(data); - notifyChannelListUpdated(); - } - } - @Override - public void onInputRemoved(String inputId) { - boolean channelRemoved = false; - ArrayList<ChannelWrapper> removedChannels = new ArrayList<>(); - for (ChannelWrapper channel : mData.channelWrapperMap.values()) { - if (channel.mChannel.getInputId().equals(inputId)) { - channel.mInputRemoved = true; - channelRemoved = true; - removedChannels.add(channel); - } - } - if (channelRemoved) { - ChannelData data = new ChannelData(); - data.channelWrapperMap.putAll(mData.channelWrapperMap); - for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) { - if (!channelWrapper.mInputRemoved) { - addChannel(data, channelWrapper.mChannel); + @Override + public void onInputRemoved(String inputId) { + boolean channelRemoved = false; + ArrayList<ChannelWrapper> removedChannels = new ArrayList<>(); + for (ChannelWrapper channel : mData.channelWrapperMap.values()) { + if (channel.mChannel.getInputId().equals(inputId)) { + channel.mInputRemoved = true; + channelRemoved = true; + removedChannels.add(channel); + } + } + if (channelRemoved) { + ChannelData data = new ChannelData(); + data.channelWrapperMap.putAll(mData.channelWrapperMap); + for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) { + if (!channelWrapper.mInputRemoved) { + addChannel(data, channelWrapper.mChannel); + } + } + Collections.sort(data.channels, mChannelComparator); + mData = new UnmodifiableChannelData(data); + notifyChannelListUpdated(); + for (ChannelWrapper channel : removedChannels) { + channel.notifyChannelRemoved(); + } } } - Collections.sort(data.channels, mChannelComparator); - mData = new UnmodifiableChannelData(data); - notifyChannelListUpdated(); - for (ChannelWrapper channel : removedChannels) { - channel.notifyChannelRemoved(); - } - } - } - }; + }; @MainThread public ChannelDataManager(Context context, TvInputManagerHelper inputManager) { - this(context, inputManager, context.getContentResolver()); + this( + context, + inputManager, + TvSingletons.getSingletons(context).getDbExecutor(), + context.getContentResolver()); } @MainThread @VisibleForTesting - ChannelDataManager(Context context, TvInputManagerHelper inputManager, + ChannelDataManager( + Context context, + TvInputManagerHelper inputManager, + Executor executor, ContentResolver contentResolver) { mContext = context; mInputManager = inputManager; + mDbExecutor = executor; mContentResolver = contentResolver; - mChannelComparator = new Channel.DefaultComparator(context, inputManager); + mChannelComparator = new ChannelImpl.DefaultComparator(context, inputManager); // Detect duplicate channels while sorting. mChannelComparator.setDetectDuplicatesEnabled(true); mHandler = new ChannelDataManagerHandler(this); - mChannelObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { - mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); - } - } - }; + mChannelObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); + } + } + }; mStoreBrowsableInSharedPreferences = !PermissionUtils.hasAccessAllEpg(mContext); - mBrowsableSharedPreferences = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_BROWSABLE, Context.MODE_PRIVATE); + mBrowsableSharedPreferences = + context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_BROWSABLE, Context.MODE_PRIVATE); } @VisibleForTesting @@ -175,9 +187,7 @@ public class ChannelDataManager { return mChannelObserver; } - /** - * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. - */ + /** Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. */ @MainThread public void start() { if (mStarted) { @@ -187,8 +197,8 @@ public class ChannelDataManager { // Should be called directly instead of posting MSG_UPDATE_CHANNELS message to the handler. // If not, other DB tasks can be executed before channel loading. handleUpdateChannels(); - mContentResolver.registerContentObserver(TvContract.Channels.CONTENT_URI, true, - mChannelObserver); + mContentResolver.registerContentObserver( + TvContract.Channels.CONTENT_URI, true, mChannelObserver); mInputManager.addCallback(mTvInputCallback); } @@ -218,9 +228,7 @@ public class ChannelDataManager { applyUpdatedValuesToDb(); } - /** - * Adds a {@link Listener}. - */ + /** Adds a {@link Listener}. */ public void addListener(Listener listener) { if (DEBUG) Log.d(TAG, "addListener " + listener); SoftPreconditions.checkNotNull(listener); @@ -229,9 +237,7 @@ public class ChannelDataManager { } } - /** - * Removes a {@link Listener}. - */ + /** Removes a {@link Listener}. */ public void removeListener(Listener listener) { if (DEBUG) Log.d(TAG, "removeListener " + listener); SoftPreconditions.checkNotNull(listener); @@ -252,8 +258,8 @@ public class ChannelDataManager { } /** - * Removes a {@link ChannelListener} for a specific channel with the channel ID - * {@code channelId}. + * Removes a {@link ChannelListener} for a specific channel with the channel ID {@code + * channelId}. */ public void removeChannelListener(Long channelId, ChannelListener listener) { ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); @@ -263,30 +269,22 @@ public class ChannelDataManager { channelWrapper.removeListener(listener); } - /** - * Checks whether data is ready. - */ + /** Checks whether data is ready. */ public boolean isDbLoadFinished() { return mDbLoadFinished; } - /** - * Returns the number of channels. - */ + /** Returns the number of channels. */ public int getChannelCount() { return mData.channels.size(); } - /** - * Returns a list of channels. - */ + /** Returns a list of channels. */ public List<Channel> getChannelList() { return new ArrayList<>(mData.channels); } - /** - * Returns a list of browsable channels. - */ + /** Returns a list of browsable channels. */ public List<Channel> getBrowsableChannelList() { List<Channel> channels = new ArrayList<>(); for (Channel channel : mData.channels) { @@ -329,9 +327,7 @@ public class ChannelDataManager { return true; } - /** - * Gets the channel with the channel ID {@code channelId}. - */ + /** Gets the channel with the channel ID {@code channelId}. */ public Channel getChannel(Long channelId) { ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null || channelWrapper.mInputRemoved) { @@ -340,9 +336,7 @@ public class ChannelDataManager { return channelWrapper.mChannel; } - /** - * The value change will be applied to DB when applyPendingDbOperation is called. - */ + /** The value change will be applied to DB when applyPendingDbOperation is called. */ public void updateBrowsable(Long channelId, boolean browsable) { updateBrowsable(channelId, browsable, false); } @@ -351,12 +345,12 @@ public class ChannelDataManager { * The value change will be applied to DB when applyPendingDbOperation is called. * * @param skipNotifyChannelBrowsableChanged If it's true, {@link Listener - * #onChannelBrowsableChanged()} is not called, when this method is called. - * {@link #notifyChannelBrowsableChanged} should be directly called, once browsable - * update is completed. + * #onChannelBrowsableChanged()} is not called, when this method is called. {@link + * #notifyChannelBrowsableChanged} should be directly called, once browsable update is + * completed. */ - public void updateBrowsable(Long channelId, boolean browsable, - boolean skipNotifyChannelBrowsableChanged) { + public void updateBrowsable( + Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged) { ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { return; @@ -396,10 +390,7 @@ public class ChannelDataManager { } } - /** - * Updates channels from DB. Once the update is done, {@code postRunnable} will - * be called. - */ + /** Updates channels from DB. Once the update is done, {@code postRunnable} will be called. */ public void updateChannels(Runnable postRunnable) { if (mChannelsUpdateTask != null) { mChannelsUpdateTask.cancel(true); @@ -411,9 +402,7 @@ public class ChannelDataManager { } } - /** - * The value change will be applied to DB when applyPendingDbOperation is called. - */ + /** The value change will be applied to DB when applyPendingDbOperation is called. */ public void updateLocked(Long channelId, boolean locked) { ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { @@ -430,10 +419,7 @@ public class ChannelDataManager { } } - /** - * Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked} - * to DB. - */ + /** Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked} to DB. */ public void applyUpdatedValuesToDb() { ChannelData data = mData; ArrayList<Long> browsableIds = new ArrayList<>(); @@ -493,11 +479,17 @@ public class ChannelDataManager { } mLockedUpdateChannelIds.clear(); if (DEBUG) { - Log.d(TAG, "applyUpdatedValuesToDb" - + "\n browsableIds size:" + browsableIds.size() - + "\n unbrowsableIds size:" + unbrowsableIds.size() - + "\n lockedIds size:" + lockedIds.size() - + "\n unlockedIds size:" + unlockedIds.size()); + Log.d( + TAG, + "applyUpdatedValuesToDb" + + "\n browsableIds size:" + + browsableIds.size() + + "\n unbrowsableIds size:" + + unbrowsableIds.size() + + "\n lockedIds size:" + + lockedIds.size() + + "\n unlockedIds size:" + + unlockedIds.size()); } } @@ -527,48 +519,34 @@ public class ChannelDataManager { mChannelsUpdateTask.executeOnDbThread(); } - /** - * Reloads channel data. - */ + /** Reloads channel data. */ public void reload() { if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); } } - /** - * A listener for ChannelDataManager. The callbacks are called on the main thread. - */ + /** A listener for ChannelDataManager. The callbacks are called on the main thread. */ public interface Listener { - /** - * Called when data load is finished. - */ + /** Called when data load is finished. */ void onLoadFinished(); /** - * Called when channels are added, deleted, or updated. But, when browsable is changed, - * it won't be called. Instead, {@link #onChannelBrowsableChanged} will be called. + * Called when channels are added, deleted, or updated. But, when browsable is changed, it + * won't be called. Instead, {@link #onChannelBrowsableChanged} will be called. */ void onChannelListUpdated(); - /** - * Called when browsable of channels are changed. - */ + /** Called when browsable of channels are changed. */ void onChannelBrowsableChanged(); } - /** - * A listener for individual channel change. The callbacks are called on the main thread. - */ + /** A listener for individual channel change. The callbacks are called on the main thread. */ public interface ChannelListener { - /** - * Called when the channel has been removed in DB. - */ + /** Called when the channel has been removed in DB. */ void onChannelRemoved(Channel channel); - /** - * Called when values of the channel has been changed. - */ + /** Called when values of the channel has been changed. */ void onChannelUpdated(Channel channel); } @@ -616,8 +594,10 @@ public class ChannelDataManager { @Override protected Boolean doInBackground(Void... params) { - try (AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor( - TvContract.buildChannelLogoUri(mChannel.getId()), "r")) { + try (AssetFileDescriptor f = + mContext.getContentResolver() + .openAssetFileDescriptor( + TvContract.buildChannelLogoUri(mChannel.getId()), "r")) { return true; } catch (SQLiteException | IOException | NullPointerException e) { // File not found or asset file not found. @@ -637,7 +617,7 @@ public class ChannelDataManager { private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { QueryAllChannelsTask(ContentResolver contentResolver) { - super(contentResolver); + super(mDbExecutor, contentResolver); } @Override @@ -663,8 +643,8 @@ public class ChannelDataManager { for (Channel channel : channels) { if (mStoreBrowsableInSharedPreferences) { String browsableKey = getBrowsableKey(channel); - channel.setBrowsable(mBrowsableSharedPreferences.getBoolean(browsableKey, - false)); + channel.setBrowsable( + mBrowsableSharedPreferences.getBoolean(browsableKey, false)); deletedBrowsableMap.remove(browsableKey); } long channelId = channel.getId(); @@ -698,7 +678,8 @@ public class ChannelDataManager { } } } - if (mStoreBrowsableInSharedPreferences && !deletedBrowsableMap.isEmpty() + if (mStoreBrowsableInSharedPreferences + && !deletedBrowsableMap.isEmpty() && PermissionUtils.hasReadTvListings(mContext)) { // If hasReadTvListings(mContext) is false, the given channel list would // empty. In this case, we skip the browsable data clean up process. @@ -745,24 +726,26 @@ public class ChannelDataManager { } /** - * Updates a column {@code columnName} of DB table {@code uri} with the value - * {@code columnValue}. The selective rows in the ID list {@code ids} will be updated. - * The DB operations will run on {@link AsyncDbTask#getExecutor()}. + * Updates a column {@code columnName} of DB table {@code uri} with the value {@code + * columnValue}. The selective rows in the ID list {@code ids} will be updated. The DB + * operations will run on {@link TvSingletons#getDbExecutor()}. */ private void updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids) { if (!PermissionUtils.hasAccessAllEpg(mContext)) { return; } - AsyncDbTask.executeOnDbThread(new Runnable() { - @Override - public void run() { - String selection = Utils.buildSelectionForIds(Channels._ID, ids); - ContentValues values = new ContentValues(); - values.put(columnName, columnValue); - mContentResolver.update(TvContract.Channels.CONTENT_URI, values, selection, null); - } - }); + mDbExecutor.execute( + new Runnable() { + @Override + public void run() { + String selection = Utils.buildSelectionForIds(Channels._ID, ids); + ContentValues values = new ContentValues(); + values.put(columnName, columnValue); + mContentResolver.update( + TvContract.Channels.CONTENT_URI, values, selection, null); + } + }); } private String getBrowsableKey(Channel channel) { @@ -784,9 +767,8 @@ public class ChannelDataManager { } /** - * Container class which includes channel data that needs to be synced. This class is - * modifiable and used for changing channel data. - * e.g. TvInputCallback, or AsyncDbTask.onPostExecute. + * Container class which includes channel data that needs to be synced. This class is modifiable + * and used for changing channel data. e.g. TvInputCallback, or AsyncDbTask.onPostExecute. */ @MainThread private static class ChannelData { @@ -806,8 +788,10 @@ public class ChannelDataManager { channels = new ArrayList<>(data.channels); } - ChannelData(Map<Long, ChannelWrapper> channelWrapperMap, - Map<String, MutableInt> channelCountMap, List<Channel> channels) { + ChannelData( + Map<Long, ChannelWrapper> channelWrapperMap, + Map<String, MutableInt> channelCountMap, + List<Channel> channels) { this.channelWrapperMap = channelWrapperMap; this.channelCountMap = channelCountMap; this.channels = channels; @@ -818,13 +802,15 @@ public class ChannelDataManager { @MainThread private static class UnmodifiableChannelData extends ChannelData { UnmodifiableChannelData() { - super(Collections.unmodifiableMap(new HashMap<>()), + super( + Collections.unmodifiableMap(new HashMap<>()), Collections.unmodifiableMap(new HashMap<>()), Collections.unmodifiableList(new ArrayList<>())); } UnmodifiableChannelData(ChannelData data) { - super(Collections.unmodifiableMap(data.channelWrapperMap), + super( + Collections.unmodifiableMap(data.channelWrapperMap), Collections.unmodifiableMap(data.channelCountMap), Collections.unmodifiableList(data.channels)); } diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/ChannelImpl.java index 4a391ae7..703f69c9 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/ChannelImpl.java @@ -28,93 +28,63 @@ import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.common.TvCommonConstants; -import com.android.tv.util.ImageLoader; +import com.android.tv.common.CommonConstants; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - +import com.android.tv.util.images.ImageLoader; import java.net.URISyntaxException; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Objects; -/** - * A convenience class to create and insert channel entries into the database. - */ -public final class Channel { - private static final String TAG = "Channel"; +/** A convenience class to create and insert channel entries into the database. */ +public final class ChannelImpl implements Channel { + private static final String TAG = "ChannelImpl"; - public static final long INVALID_ID = -1; - public static final int LOAD_IMAGE_TYPE_CHANNEL_LOGO = 1; - public static final int LOAD_IMAGE_TYPE_APP_LINK_ICON = 2; - public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; - - /** - * Compares the channel numbers of channels which belong to the same input. - */ - public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } - }; - - /** - * When a TIS doesn't provide any information about app link, and it doesn't have a leanback - * launch intent, there will be no app link card for the TIS. - */ - public static final int APP_LINK_TYPE_NONE = -1; - /** - * When a TIS provide a specific app link information, the app link card will be - * {@code APP_LINK_TYPE_CHANNEL} which contains all the provided information. - */ - public static final int APP_LINK_TYPE_CHANNEL = 1; - /** - * When a TIS doesn't provide a specific app link information, but the app has a leanback launch - * intent, the app link card will be {@code APP_LINK_TYPE_APP} which launches the application. - */ - public static final int APP_LINK_TYPE_APP = 2; + /** Compares the channel numbers of channels which belong to the same input. */ + public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = + new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } + }; private static final int APP_LINK_TYPE_NOT_SET = 0; private static final String INVALID_PACKAGE_NAME = "packageName"; public static final String[] PROJECTION = { - // Columns must match what is read in Channel.fromCursor() - TvContract.Channels._ID, - TvContract.Channels.COLUMN_PACKAGE_NAME, - TvContract.Channels.COLUMN_INPUT_ID, - TvContract.Channels.COLUMN_TYPE, - TvContract.Channels.COLUMN_DISPLAY_NUMBER, - TvContract.Channels.COLUMN_DISPLAY_NAME, - TvContract.Channels.COLUMN_DESCRIPTION, - TvContract.Channels.COLUMN_VIDEO_FORMAT, - TvContract.Channels.COLUMN_BROWSABLE, - TvContract.Channels.COLUMN_SEARCHABLE, - TvContract.Channels.COLUMN_LOCKED, - TvContract.Channels.COLUMN_APP_LINK_TEXT, - TvContract.Channels.COLUMN_APP_LINK_COLOR, - TvContract.Channels.COLUMN_APP_LINK_ICON_URI, - TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, - TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input + // Columns must match what is read in ChannelImpl.fromCursor() + TvContract.Channels._ID, + TvContract.Channels.COLUMN_PACKAGE_NAME, + TvContract.Channels.COLUMN_INPUT_ID, + TvContract.Channels.COLUMN_TYPE, + TvContract.Channels.COLUMN_DISPLAY_NUMBER, + TvContract.Channels.COLUMN_DISPLAY_NAME, + TvContract.Channels.COLUMN_DESCRIPTION, + TvContract.Channels.COLUMN_VIDEO_FORMAT, + TvContract.Channels.COLUMN_BROWSABLE, + TvContract.Channels.COLUMN_SEARCHABLE, + TvContract.Channels.COLUMN_LOCKED, + TvContract.Channels.COLUMN_APP_LINK_TEXT, + TvContract.Channels.COLUMN_APP_LINK_COLOR, + TvContract.Channels.COLUMN_APP_LINK_ICON_URI, + TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, + TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, + TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input }; /** - * Channel number delimiter between major and minor parts. - */ - public static final char CHANNEL_NUMBER_DELIMITER = '-'; - - /** - * Creates {@code Channel} object from cursor. + * Creates {@code ChannelImpl} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION} - * */ - public static Channel fromCursor(Cursor cursor) { + public static ChannelImpl fromCursor(Cursor cursor) { // Columns read must match the order of {@link #PROJECTION} - Channel channel = new Channel(); + ChannelImpl channel = new ChannelImpl(); int index = 0; channel.mId = cursor.getLong(index++); channel.mPackageName = Utils.intern(cursor.getString(index++)); @@ -132,21 +102,20 @@ public final class Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); - if (Utils.isBundledInput(channel.mInputId)) { + if (CommonUtils.isBundledInput(channel.mInputId)) { channel.mRecordingProhibited = cursor.getInt(index++) != 0; } return channel; } - /** - * Replaces the channel number separator with dash('-'). - */ + /** Replaces the channel number separator with dash('-'). */ public static String normalizeDisplayNumber(String string) { if (!TextUtils.isEmpty(string)) { int length = string.length(); for (int i = 0; i < length; i++) { char c = string.charAt(i); - if (c == '.' || Character.isWhitespace(c) + if (c == '.' + || Character.isWhitespace(c) || Character.getType(c) == Character.DASH_PUNCTUATION) { StringBuilder sb = new StringBuilder(string); sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER); @@ -183,14 +152,16 @@ public final class Channel { private boolean mChannelLogoExist; - private Channel() { + private ChannelImpl() { // Do nothing. } + @Override public long getId() { return mId; } + @Override public Uri getUri() { if (isPassthrough()) { return TvContract.buildChannelUriForPassthroughInput(mInputId); @@ -199,97 +170,110 @@ public final class Channel { } } + @Override public String getPackageName() { return mPackageName; } + @Override public String getInputId() { return mInputId; } + @Override public String getType() { return mType; } + @Override public String getDisplayNumber() { return mDisplayNumber; } + @Override @Nullable public String getDisplayName() { return mDisplayName; } + @Override public String getDescription() { return mDescription; } + @Override public String getVideoFormat() { return mVideoFormat; } + @Override public boolean isPassthrough() { return mIsPassthrough; } /** - * Gets identification text for displaying or debugging. - * It's made from Channels' display number plus their display name. + * Gets identification text for displaying or debugging. It's made from Channels' display number + * plus their display name. */ + @Override public String getDisplayText() { - return TextUtils.isEmpty(mDisplayName) ? mDisplayNumber + return TextUtils.isEmpty(mDisplayName) + ? mDisplayNumber : mDisplayNumber + " " + mDisplayName; } + @Override public String getAppLinkText() { return mAppLinkText; } + @Override public int getAppLinkColor() { return mAppLinkColor; } + @Override public String getAppLinkIconUri() { return mAppLinkIconUri; } + @Override public String getAppLinkPosterArtUri() { return mAppLinkPosterArtUri; } + @Override public String getAppLinkIntentUri() { return mAppLinkIntentUri; } - /** - * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. - */ + /** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */ + @Override public String getLogoUri() { return mLogoUri; } + @Override public boolean isRecordingProhibited() { return mRecordingProhibited; } - /** - * Checks whether this channel is physical tuner channel or not. - */ + /** Checks whether this channel is physical tuner channel or not. */ + @Override public boolean isPhysicalTunerChannel() { return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType); } - /** - * Checks if two channels equal by checking ids. - */ + /** Checks if two channels equal by checking ids. */ @Override public boolean equals(Object o) { - if (!(o instanceof Channel)) { + if (!(o instanceof ChannelImpl)) { return false; } - Channel other = (Channel) o; + ChannelImpl other = (ChannelImpl) o; // All pass-through TV channels have INVALID_ID value for mId. - return mId == other.mId && TextUtils.equals(mInputId, other.mInputId) + return mId == other.mId + && TextUtils.equals(mInputId, other.mInputId) && mIsPassthrough == other.mIsPassthrough; } @@ -298,15 +282,18 @@ public final class Channel { return Objects.hash(mId, mInputId, mIsPassthrough); } + @Override public boolean isBrowsable() { return mBrowsable; } /** Checks whether this channel is searchable or not. */ + @Override public boolean isSearchable() { return mSearchable; } + @Override public boolean isLocked() { return mLocked; } @@ -319,9 +306,7 @@ public final class Channel { mLocked = locked; } - /** - * Sets channel logo uri which is got from cloud. - */ + /** Sets channel logo uri which is got from cloud. */ public void setLogoUri(String logoUri) { mLogoUri = logoUri; } @@ -331,45 +316,91 @@ public final class Channel { * channels have same logos. It also excludes browsable and locked, because two fields are * changed by TV app. */ + @Override public boolean hasSameReadOnlyInfo(Channel other) { return other != null - && Objects.equals(mId, other.mId) - && Objects.equals(mPackageName, other.mPackageName) - && Objects.equals(mInputId, other.mInputId) - && Objects.equals(mType, other.mType) - && Objects.equals(mDisplayNumber, other.mDisplayNumber) - && Objects.equals(mDisplayName, other.mDisplayName) - && Objects.equals(mDescription, other.mDescription) - && Objects.equals(mVideoFormat, other.mVideoFormat) - && mIsPassthrough == other.mIsPassthrough - && Objects.equals(mAppLinkText, other.mAppLinkText) - && mAppLinkColor == other.mAppLinkColor - && Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri) - && Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri) - && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri) - && Objects.equals(mRecordingProhibited, other.mRecordingProhibited); + && Objects.equals(mId, other.getId()) + && Objects.equals(mPackageName, other.getPackageName()) + && Objects.equals(mInputId, other.getInputId()) + && Objects.equals(mType, other.getType()) + && Objects.equals(mDisplayNumber, other.getDisplayNumber()) + && Objects.equals(mDisplayName, other.getDisplayName()) + && Objects.equals(mDescription, other.getDescription()) + && Objects.equals(mVideoFormat, other.getVideoFormat()) + && mIsPassthrough == other.isPassthrough() + && Objects.equals(mAppLinkText, other.getAppLinkText()) + && mAppLinkColor == other.getAppLinkColor() + && Objects.equals(mAppLinkIconUri, other.getAppLinkIconUri()) + && Objects.equals(mAppLinkPosterArtUri, other.getAppLinkPosterArtUri()) + && Objects.equals(mAppLinkIntentUri, other.getAppLinkIntentUri()) + && Objects.equals(mRecordingProhibited, other.isRecordingProhibited()); } @Override public String toString() { return "Channel{" - + "id=" + mId - + ", packageName=" + mPackageName - + ", inputId=" + mInputId - + ", type=" + mType - + ", displayNumber=" + mDisplayNumber - + ", displayName=" + mDisplayName - + ", description=" + mDescription - + ", videoFormat=" + mVideoFormat - + ", isPassthrough=" + mIsPassthrough - + ", browsable=" + mBrowsable - + ", searchable=" + mSearchable - + ", locked=" + mLocked - + ", appLinkText=" + mAppLinkText - + ", recordingProhibited=" + mRecordingProhibited + "}"; - } - - void copyFrom(Channel other) { + + "id=" + + mId + + ", packageName=" + + mPackageName + + ", inputId=" + + mInputId + + ", type=" + + mType + + ", displayNumber=" + + mDisplayNumber + + ", displayName=" + + mDisplayName + + ", description=" + + mDescription + + ", videoFormat=" + + mVideoFormat + + ", isPassthrough=" + + mIsPassthrough + + ", browsable=" + + mBrowsable + + ", searchable=" + + mSearchable + + ", locked=" + + mLocked + + ", appLinkText=" + + mAppLinkText + + ", recordingProhibited=" + + mRecordingProhibited + + "}"; + } + + @Override + public void copyFrom(Channel channel) { + if (channel instanceof ChannelImpl) { + copyFrom((ChannelImpl) channel); + } else { + // copy what we can + mId = channel.getId(); + mPackageName = channel.getPackageName(); + mInputId = channel.getInputId(); + mType = channel.getType(); + mDisplayNumber = channel.getDisplayNumber(); + mDisplayName = channel.getDisplayName(); + mDescription = channel.getDescription(); + mVideoFormat = channel.getVideoFormat(); + mIsPassthrough = channel.isPassthrough(); + mBrowsable = channel.isBrowsable(); + mSearchable = channel.isSearchable(); + mLocked = channel.isLocked(); + mAppLinkText = channel.getAppLinkText(); + mAppLinkColor = channel.getAppLinkColor(); + mAppLinkIconUri = channel.getAppLinkIconUri(); + mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri(); + mAppLinkIntentUri = channel.getAppLinkIntentUri(); + mRecordingProhibited = channel.isRecordingProhibited(); + mChannelLogoExist = channel.channelLogoExists(); + } + } + + @SuppressWarnings("ReferenceEquality") + public void copyFrom(ChannelImpl channel) { + ChannelImpl other = (ChannelImpl) channel; if (this == other) { return; } @@ -396,10 +427,8 @@ public final class Channel { mChannelLogoExist = other.mChannelLogoExist; } - /** - * Creates a channel for a passthrough TV input. - */ - public static Channel createPassthroughChannel(Uri uri) { + /** Creates a channel for a passthrough TV input. */ + public static ChannelImpl createPassthroughChannel(Uri uri) { if (!TvContract.isChannelUriForPassthroughInput(uri)) { throw new IllegalArgumentException("URI is not a passthrough channel URI"); } @@ -407,33 +436,25 @@ public final class Channel { return createPassthroughChannel(inputId); } - /** - * Creates a channel for a passthrough TV input with {@code inputId}. - */ - public static Channel createPassthroughChannel(String inputId) { - return new Builder() - .setInputId(inputId) - .setPassthrough(true) - .build(); + /** Creates a channel for a passthrough TV input with {@code inputId}. */ + public static ChannelImpl createPassthroughChannel(String inputId) { + return new Builder().setInputId(inputId).setPassthrough(true).build(); } - /** - * Checks whether the channel is valid or not. - */ + /** Checks whether the channel is valid or not. */ public static boolean isValid(Channel channel) { - return channel != null && (channel.mId != INVALID_ID || channel.mIsPassthrough); + return channel != null && (channel.getId() != INVALID_ID || channel.isPassthrough()); } /** - * Builder class for {@code Channel}. - * Suppress using this outside of ChannelDataManager - * so Channels could be managed by ChannelDataManager. + * Builder class for {@code ChannelImpl}. Suppress using this outside of ChannelDataManager so + * Channels could be managed by ChannelDataManager. */ public static final class Builder { - private final Channel mChannel; + private final ChannelImpl mChannel; public Builder() { - mChannel = new Channel(); + mChannel = new ChannelImpl(); // Fill initial data. mChannel.mId = INVALID_ID; mChannel.mPackageName = INVALID_PACKAGE_NAME; @@ -447,7 +468,7 @@ public final class Channel { } public Builder(Channel other) { - mChannel = new Channel(); + mChannel = new ChannelImpl(); mChannel.copyFrom(other); } @@ -548,16 +569,14 @@ public final class Channel { return this; } - public Channel build() { - Channel channel = new Channel(); + public ChannelImpl build() { + ChannelImpl channel = new ChannelImpl(); channel.copyFrom(mChannel); return channel; } } - /** - * Prefetches the images for this channel. - */ + /** Prefetches the images for this channel. */ public void prefetchImage(Context context, int type, int maxWidth, int maxHeight) { String uriString = getImageUriString(type); if (!TextUtils.isEmpty(uriString)) { @@ -566,46 +585,49 @@ public final class Channel { } /** - * Loads the bitmap of this channel and returns it via {@code callback}. - * The loaded bitmap will be cached and resized with given params. - * <p> - * Note that it may directly call {@code callback} if the bitmap is already loaded. + * Loads the bitmap of this channel and returns it via {@code callback}. The loaded bitmap will + * be cached and resized with given params. + * + * <p>Note that it may directly call {@code callback} if the bitmap is already loaded. * * @param context A context. - * @param type The type of bitmap which will be loaded. It should be one of follows: - * {@link #LOAD_IMAGE_TYPE_CHANNEL_LOGO}, {@link #LOAD_IMAGE_TYPE_APP_LINK_ICON}, or - * {@link #LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART}. + * @param type The type of bitmap which will be loaded. It should be one of follows: {@link + * Channel#LOAD_IMAGE_TYPE_CHANNEL_LOGO}, {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_ICON}, or + * {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART}. * @param maxWidth The max width of the loaded bitmap. * @param maxHeight The max height of the loaded bitmap. * @param callback A callback which will be called after the loading finished. */ @UiThread - public void loadBitmap(Context context, final int type, int maxWidth, int maxHeight, + public void loadBitmap( + Context context, + final int type, + int maxWidth, + int maxHeight, ImageLoader.ImageLoaderCallback callback) { String uriString = getImageUriString(type); ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback); } /** - * Sets if the channel logo exists. This method should be only called from - * {@link ChannelDataManager}. + * Sets if the channel logo exists. This method should be only called from {@link + * ChannelDataManager}. */ - void setChannelLogoExist(boolean exist) { + @Override + public void setChannelLogoExist(boolean exist) { mChannelLogoExist = exist; } - /** - * Returns if channel logo exists. - */ + /** Returns if channel logo exists. */ public boolean channelLogoExists() { return mChannelLogoExist; } /** - * Returns the type of app link for this channel. - * It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and - * a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which - * holds the channel has leanback launch intent, and it returns {@link #APP_LINK_TYPE_NONE} + * Returns the type of app link for this channel. It returns {@link + * Channel#APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and a valid app + * link intent, it returns {@link Channel#APP_LINK_TYPE_APP} if the input service which holds + * the channel has leanback launch intent, and it returns {@link Channel#APP_LINK_TYPE_NONE} * otherwise. */ public int getAppLinkType(Context context) { @@ -616,8 +638,8 @@ public final class Channel { } /** - * Returns the app link intent for this channel. - * If the type of app link is {@link #APP_LINK_TYPE_NONE}, it returns {@code null}. + * Returns the app link intent for this channel. If the type of app link is {@link + * Channel#APP_LINK_TYPE_NONE}, it returns {@code null}. */ public Intent getAppLinkIntent(Context context) { if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { @@ -635,8 +657,8 @@ public final class Channel { Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); if (intent.resolveActivityInfo(pm, 0) != null) { mAppLinkIntent = intent; - mAppLinkIntent.putExtra(TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, - getUri().toString()); + mAppLinkIntent.putExtra( + CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); mAppLinkType = APP_LINK_TYPE_CHANNEL; return; } else { @@ -652,8 +674,8 @@ public final class Channel { } mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName); if (mAppLinkIntent != null) { - mAppLinkIntent.putExtra(TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, - getUri().toString()); + mAppLinkIntent.putExtra( + CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); mAppLinkType = APP_LINK_TYPE_APP; } } @@ -670,6 +692,17 @@ public final class Channel { return null; } + /** + * Default Channel ordering. + * + * <p>Ordering + * <li>{@link TvInputManagerHelper#isPartnerInput(String)} + * <li>{@link #getInputLabelForChannel(Channel)} + * <li>{@link #getInputId()} + * <li>{@link ChannelNumber#compare(String, String)} + * <li> + * </ol> + */ public static class DefaultComparator implements Comparator<Channel> { private final Context mContext; private final TvInputManagerHelper mInputManager; @@ -685,6 +718,7 @@ public final class Channel { mDetectDuplicatesEnabled = detectDuplicatesEnabled; } + @SuppressWarnings("ReferenceEquality") @Override public int compare(Channel lhs, Channel rhs) { if (lhs == rhs) { @@ -699,8 +733,10 @@ public final class Channel { // Compare the input labels. String lhsLabel = getInputLabelForChannel(lhs); String rhsLabel = getInputLabelForChannel(rhs); - int result = lhsLabel == null ? (rhsLabel == null ? 0 : 1) : rhsLabel == null ? -1 - : lhsLabel.compareTo(rhsLabel); + int result = + lhsLabel == null + ? (rhsLabel == null ? 0 : 1) + : rhsLabel == null ? -1 : lhsLabel.compareTo(rhsLabel); if (result != 0) { return result; } @@ -712,8 +748,13 @@ public final class Channel { // Compare the channel numbers if both channels belong to the same input. result = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); if (mDetectDuplicatesEnabled && result == 0) { - Log.w(TAG, "Duplicate channels detected! - \"" - + lhs.getDisplayText() + "\" and \"" + rhs.getDisplayText() + "\""); + Log.w( + TAG, + "Duplicate channels detected! - \"" + + lhs.getDisplayText() + + "\" and \"" + + rhs.getDisplayText() + + "\""); } return result; } @@ -733,4 +774,4 @@ public final class Channel { return label; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 132cab7a..89d1e36c 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -28,17 +28,16 @@ import android.os.RemoteException; import android.support.annotation.MainThread; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.util.BitmapUtils; -import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; -import com.android.tv.util.PermissionUtils; - +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.common.util.SharedPreferencesUtils; +import com.android.tv.data.api.Channel; +import com.android.tv.util.images.BitmapUtils; +import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Map; import java.util.List; +import java.util.Map; /** * Fetches channel logos from the cloud into the database. It's for the channels which have no logos @@ -54,12 +53,11 @@ public class ChannelLogoFetcher { private static FetchLogoTask sFetchTask; /** - * Fetches the channel logos from the cloud data and insert them into TvProvider. - * The previous task is canceled and a new task starts. + * Fetches the channel logos from the cloud data and insert them into TvProvider. The previous + * task is canceled and a new task starts. */ @MainThread - public static void startFetchingChannelLogos( - Context context, List<Channel> channels) { + public static void startFetchingChannelLogos(Context context, List<Channel> channels) { if (!PermissionUtils.hasAccessAllEpg(context)) { // TODO: support this feature for non-system LC app. b/23939816 return; @@ -76,8 +74,7 @@ public class ChannelLogoFetcher { sFetchTask.execute(); } - private ChannelLogoFetcher() { - } + private ChannelLogoFetcher() {} private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> { private final Context mContext; @@ -105,8 +102,8 @@ public class ChannelLogoFetcher { Context.MODE_PRIVATE); SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); Map<String, ?> uncheckedChannels = sharedPreferences.getAll(); - boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean( - PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); + boolean isFirstTimeFetchChannelLogo = + sharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); // Iterating channels. for (Channel channel : mChannels) { String channelIdString = Long.toString(channel.getId()); @@ -117,7 +114,7 @@ public class ChannelLogoFetcher { sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri()); } else if (TextUtils.isEmpty(channel.getLogoUri()) && (!TextUtils.isEmpty(storedChannelLogoUri) - || isFirstTimeFetchChannelLogo)) { + || isFirstTimeFetchChannelLogo)) { channelsToRemove.add(channel); sharedPreferencesEditor.remove(channelIdString); } @@ -136,11 +133,18 @@ public class ChannelLogoFetcher { } // Downloads the channel logo. String logoUri = channel.getLogoUri(); - ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString( - mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); + ScaledBitmapInfo bitmapInfo = + BitmapUtils.decodeSampledBitmapFromUriString( + mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); if (bitmapInfo == null) { - Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName() - + ", " + "logoUri=" + logoUri + "}"); + Log.e( + TAG, + "Failed to load bitmap. {channelName=" + + channel.getDisplayName() + + ", " + + "logoUri=" + + logoUri + + "}"); continue; } if (isCancelled()) { @@ -160,8 +164,13 @@ public class ChannelLogoFetcher { continue; } if (DEBUG) { - Log.d(TAG, "Inserting logo file to DB succeeded. {from=" + logoUri + ", to=" - + dstLogoUri + "}"); + Log.d( + TAG, + "Inserting logo file to DB succeeded. {from=" + + logoUri + + ", to=" + + dstLogoUri + + "}"); } } @@ -171,8 +180,10 @@ public class ChannelLogoFetcher { if (!channelsToRemove.isEmpty()) { ArrayList<ContentProviderOperation> ops = new ArrayList<>(); for (Channel channel : channelsToRemove) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildChannelLogoUri(channel.getId())).build()); + ops.add( + ContentProviderOperation.newDelete( + TvContract.buildChannelLogoUri(channel.getId())) + .build()); } try { mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 29054aa5..afdcc580 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -19,18 +19,18 @@ package com.android.tv.data; import android.support.annotation.NonNull; import android.text.TextUtils; import android.view.KeyEvent; - -import com.android.tv.util.StringUtils; - +import com.android.tv.common.util.StringUtils; +import com.android.tv.data.api.Channel; import java.util.Objects; -/** - * A convenience class to handle channel number. - */ +/** A convenience class to handle channel number. */ public final class ChannelNumber implements Comparable<ChannelNumber> { private static final int[] CHANNEL_DELIMITER_KEYCODES = { - KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD, - KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE + KeyEvent.KEYCODE_MINUS, + KeyEvent.KEYCODE_NUMPAD_SUBTRACT, + KeyEvent.KEYCODE_PERIOD, + KeyEvent.KEYCODE_NUMPAD_DOT, + KeyEvent.KEYCODE_SPACE }; /** The major part of the channel number. */ @@ -44,6 +44,23 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { reset(); } + /** + * {@code lhs} and {@code rhs} are equivalent if {@link ChannelNumber#compare(String, String)} + * is 0 or if only one has a delimiter and both {@link ChannelNumber#majorNumber} equals. + */ + public static boolean equivalent(String lhs, String rhs) { + if (compare(lhs, rhs) == 0) { + return true; + } + // Match if only one has delimiter + ChannelNumber lhsNumber = parseChannelNumber(lhs); + ChannelNumber rhsNumber = parseChannelNumber(rhs); + return lhsNumber != null + && rhsNumber != null + && lhsNumber.hasDelimiter != rhsNumber.hasDelimiter + && lhsNumber.majorNumber.equals(rhsNumber.majorNumber); + } + public void reset() { setChannelNumber("", false, ""); } @@ -68,8 +85,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { int minor = hasDelimiter ? Integer.parseInt(minorNumber) : 0; int opponentMajor = Integer.parseInt(another.majorNumber); - int opponentMinor = another.hasDelimiter - ? Integer.parseInt(another.minorNumber) : 0; + int opponentMinor = another.hasDelimiter ? Integer.parseInt(another.minorNumber) : 0; if (major == opponentMajor) { return minor - opponentMinor; } @@ -103,10 +119,10 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { /** * Returns the ChannelNumber instance. - * <p> - * Note that all the channel number argument should be normalized by - * {@link Channel#normalizeDisplayNumber}. The channels retrieved from - * {@link ChannelDataManager} are already normalized. + * + * <p>Note that all the channel number argument should be normalized by {@link + * ChannelImpl#normalizeDisplayNumber}. The channels retrieved from {@link ChannelDataManager} + * are already normalized. */ public static ChannelNumber parseChannelNumber(String number) { if (number == null) { @@ -134,10 +150,10 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { /** * Compares the channel numbers. - * <p> - * Note that all the channel number arguments should be normalized by - * {@link Channel#normalizeDisplayNumber}. The channels retrieved from - * {@link ChannelDataManager} are already normalized. + * + * <p>Note that all the channel number arguments should be normalized by {@link + * ChannelImpl#normalizeDisplayNumber}. The channels retrieved from {@link ChannelDataManager} + * are already normalized. */ public static int compare(String lhs, String rhs) { ChannelNumber lhsNumber = parseChannelNumber(lhs); @@ -156,7 +172,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { private static boolean isInteger(String string) { try { Integer.parseInt(string); - } catch(NumberFormatException | NullPointerException e) { + } catch (NumberFormatException | NullPointerException e) { return false; } return true; diff --git a/src/com/android/tv/data/DisplayMode.java b/src/com/android/tv/data/DisplayMode.java index ccba5480..dbcca010 100644 --- a/src/com/android/tv/data/DisplayMode.java +++ b/src/com/android/tv/data/DisplayMode.java @@ -17,7 +17,6 @@ package com.android.tv.data; import android.content.Context; - import com.android.tv.R; public class DisplayMode { @@ -28,12 +27,10 @@ public class DisplayMode { public static final int MODE_ZOOM = 2; public static final int SIZE_OF_RATIO_TYPES = MODE_ZOOM + 1; - /** - * Constant to indicate that any mode is not set yet. - */ + /** Constant to indicate that any mode is not set yet. */ public static final int MODE_NOT_DEFINED = -1; - private DisplayMode() { } + private DisplayMode() {} public static String getLabel(int mode, Context context) { return context.getResources().getStringArray(R.array.display_mode_labels)[mode]; diff --git a/src/com/android/tv/data/GenreItems.java b/src/com/android/tv/data/GenreItems.java index b12fd1aa..dfecb63c 100644 --- a/src/com/android/tv/data/GenreItems.java +++ b/src/com/android/tv/data/GenreItems.java @@ -18,13 +18,10 @@ package com.android.tv.data; import android.content.Context; import android.media.tv.TvContract.Programs.Genres; - import com.android.tv.R; public class GenreItems { - /** - * Genre ID indicating all channels. - */ + /** Genre ID indicating all channels. */ public static final int ID_ALL_CHANNELS = 0; private static final String[] CANONICAL_GENRES = { @@ -48,11 +45,9 @@ public class GenreItems { Genres.TECH_SCIENCE }; - private GenreItems() { } + private GenreItems() {} - /** - * Returns array of all genre labels. - */ + /** Returns array of all genre labels. */ public static String[] getLabels(Context context) { String[] items = context.getResources().getStringArray(R.array.genre_labels); if (items.length != CANONICAL_GENRES.length) { @@ -61,16 +56,14 @@ public class GenreItems { return items; } - /** - * Returns the number of genres including all channels. - */ + /** Returns the number of genres including all channels. */ public static int getGenreCount() { return CANONICAL_GENRES.length; } /** - * Returns the canonical genre for the given id. - * If the id is invalid, {@code null} will be returned instead. + * Returns the canonical genre for the given id. If the id is invalid, {@code null} will be + * returned instead. */ public static String getCanonicalGenre(int id) { if (id < 0 || id >= CANONICAL_GENRES.length) { @@ -80,8 +73,8 @@ public class GenreItems { } /** - * Returns id for the given canonical genre. - * If the genre is invalid, {@link #ID_ALL_CHANNELS} will be returned instead. + * Returns id for the given canonical genre. If the genre is invalid, {@link #ID_ALL_CHANNELS} + * will be returned instead. */ public static int getId(String canonicalGenre) { if (canonicalGenre == null) { diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java index e33ca18f..4c30d395 100644 --- a/src/com/android/tv/data/InternalDataUtils.java +++ b/src/com/android/tv/data/InternalDataUtils.java @@ -19,10 +19,8 @@ package com.android.tv.data; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; - import com.android.tv.data.Program.CriticScore; import com.android.tv.dvr.data.RecordedProgram; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -31,22 +29,22 @@ import java.io.ObjectOutputStream; import java.util.List; /** - * A utility class to parse and store data from the - * {@link android.media.tv.TvContract.Programs#COLUMN_INTERNAL_PROVIDER_DATA} field in the - * {@link android.media.tv.TvContract.Programs}. + * A utility class to parse and store data from the {@link + * android.media.tv.TvContract.Programs#COLUMN_INTERNAL_PROVIDER_DATA} field in the {@link + * android.media.tv.TvContract.Programs}. */ public final class InternalDataUtils { private static final boolean DEBUG = false; private static final String TAG = "InternalDataUtils"; private InternalDataUtils() { - //do nothing + // do nothing } /** * Deserializes a byte array into objects to be stored in the Program class. * - * <p> Series ID and critic scores are loaded from the bytes. + * <p>Series ID and critic scores are loaded from the bytes. * * @param bytes the bytes to be deserialized * @param builder the builder for the Program class @@ -70,6 +68,7 @@ public final class InternalDataUtils { /** * Convenience method for converting relevant data in Program class to a serialized blob type * for storage in internal_provider_data field. + * * @param program the program which contains the objects to be serialized * @return serialized blob-type data */ @@ -83,8 +82,10 @@ public final class InternalDataUtils { return bos.toByteArray(); } } catch (IOException e) { - Log.e(TAG, "Could not serialize internal provider contents for program: " - + program.getTitle()); + Log.e( + TAG, + "Could not serialize internal provider contents for program: " + + program.getTitle()); } return null; } @@ -92,13 +93,13 @@ public final class InternalDataUtils { /** * Deserializes a byte array into objects to be stored in the RecordedProgram class. * - * <p> Series ID is loaded from the bytes. + * <p>Series ID is loaded from the bytes. * * @param bytes the bytes to be deserialized * @param builder the builder for the RecordedProgram class */ - public static void deserializeInternalProviderData(byte[] bytes, - RecordedProgram.Builder builder) { + public static void deserializeInternalProviderData( + byte[] bytes, RecordedProgram.Builder builder) { if (bytes == null || bytes.length == 0) { return; } @@ -115,6 +116,7 @@ public final class InternalDataUtils { /** * Serializes relevant objects in {@link android.media.tv.TvContract.Programs} to byte array. + * * @return the serialized byte array */ public static byte[] serializeInternalProviderData(RecordedProgram program) { @@ -125,9 +127,11 @@ public final class InternalDataUtils { return bos.toByteArray(); } } catch (IOException e) { - Log.e(TAG, "Could not serialize internal provider contents for program: " - + program.getTitle()); + Log.e( + TAG, + "Could not serialize internal provider contents for program: " + + program.getTitle()); } return null; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/Lineup.java b/src/com/android/tv/data/Lineup.java index d0e9d7ba..4393cd3d 100644 --- a/src/com/android/tv/data/Lineup.java +++ b/src/com/android/tv/data/Lineup.java @@ -17,78 +17,94 @@ package com.android.tv.data; import android.support.annotation.IntDef; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; -/** - * A class that represents a lineup. - */ +/** A class that represents a lineup. */ public class Lineup { - /** - * The ID of this lineup. - */ - public final String id; + /** The ID of this lineup. */ + public String getId() { + return id; + } - /** - * The type associated with this lineup. - */ - public final int type; + /** The type associated with this lineup. */ + public int getType() { + return type; + } - /** - * The human readable name associated with this lineup. - */ - public final String name; + /** The human readable name associated with this lineup. */ + public String getName() { + return name; + } - /** - * Location this lineup can be found. - * This is a human readable description of a geographic location. - */ - public final String location; + /** The human readable name associated with this lineup. */ + public String getLocation() { + return location; + } + + /** An unmodifiable list of channel numbers that this lineup has. */ + public List<String> getChannels() { + return channels; + } + + private final String id; + + private final int type; + + private final String name; + + private final String location; + + private final List<String> channels; @Retention(RetentionPolicy.SOURCE) - @IntDef({LINEUP_CABLE, LINEUP_SATELLITE, LINEUP_BROADCAST_DIGITAL, LINEUP_BROADCAST_ANALOG, - LINEUP_IPTV, LINEUP_MVPD}) + @IntDef({ + LINEUP_CABLE, + LINEUP_SATELLITE, + LINEUP_BROADCAST_DIGITAL, + LINEUP_BROADCAST_ANALOG, + LINEUP_IPTV, + LINEUP_MVPD, + LINEUP_INTERNET, + LINEUP_OTHER + }) public @interface LineupType {} - /** - * Lineup type for cable. - */ + /** Lineup type for cable. */ public static final int LINEUP_CABLE = 0; - /** - * Lineup type for satelite. - */ + /** Lineup type for satelite. */ public static final int LINEUP_SATELLITE = 1; - /** - * Lineup type for broadcast digital. - */ + /** Lineup type for broadcast digital. */ public static final int LINEUP_BROADCAST_DIGITAL = 2; - /** - * Lineup type for broadcast analog. - */ + /** Lineup type for broadcast analog. */ public static final int LINEUP_BROADCAST_ANALOG = 3; - /** - * Lineup type for IPTV. - */ + /** Lineup type for IPTV. */ public static final int LINEUP_IPTV = 4; /** - * Indicates the lineup is either satelite, cable or IPTV but we are not sure which specific + * Indicates the lineup is either satellite, cable or IPTV but we are not sure which specific * type. - */ + */ public static final int LINEUP_MVPD = 5; - /** - * Creates a lineup. - */ - public Lineup(String id, int type, String name, String location) { + /** Lineup type for Internet. */ + public static final int LINEUP_INTERNET = 6; + + /** Lineup type for other. */ + public static final int LINEUP_OTHER = 7; + + /** Creates a lineup. */ + public Lineup(String id, int type, String name, String location, List<String> channels) { this.id = id; this.type = type; this.name = name; this.location = location; + this.channels = Collections.unmodifiableList(channels); } } diff --git a/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java b/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java index 77b6c9b8..edb33556 100644 --- a/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java +++ b/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java @@ -17,8 +17,6 @@ package com.android.tv.data; public interface OnCurrentProgramUpdatedListener { - /** - * Called when the current program is updated. - */ + /** Called when the current program is updated. */ void onCurrentProgramUpdated(long channelId, Program program); } diff --git a/src/com/android/tv/data/ParcelableList.java b/src/com/android/tv/data/ParcelableList.java index 78f444e4..1c1f5f29 100644 --- a/src/com/android/tv/data/ParcelableList.java +++ b/src/com/android/tv/data/ParcelableList.java @@ -18,18 +18,13 @@ package com.android.tv.data; import android.os.Parcel; import android.os.Parcelable; - import java.util.ArrayList; import java.util.Collection; import java.util.List; -/** - * A convenience class for the list of {@link Parcelable}s. - */ +/** A convenience class for the list of {@link Parcelable}s. */ public final class ParcelableList<T extends Parcelable> implements Parcelable { - /** - * Create instance from {@link Parcel}. - */ + /** Create instance from {@link Parcel}. */ public static ParcelableList fromParcel(Parcel in) { ParcelableList list = new ParcelableList(); int length = in.readInt(); @@ -41,32 +36,29 @@ public final class ParcelableList<T extends Parcelable> implements Parcelable { return list; } - /** - * A creator for {@link ParcelableList}. - */ - public static final Creator<ParcelableList> CREATOR = new Creator<ParcelableList>() { - @Override - public ParcelableList createFromParcel(Parcel in) { - return ParcelableList.fromParcel(in); - } + /** A creator for {@link ParcelableList}. */ + public static final Creator<ParcelableList> CREATOR = + new Creator<ParcelableList>() { + @Override + public ParcelableList createFromParcel(Parcel in) { + return ParcelableList.fromParcel(in); + } - @Override - public ParcelableList[] newArray(int size) { - return new ParcelableList[size]; - } - }; + @Override + public ParcelableList[] newArray(int size) { + return new ParcelableList[size]; + } + }; private final List<T> mList = new ArrayList<>(); - private ParcelableList() { } + private ParcelableList() {} public ParcelableList(Collection<T> initialList) { mList.addAll(initialList); } - /** - * Returns the list. - */ + /** Returns the list. */ public List<T> getList() { return new ArrayList<T>(mList); } diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java index 01a58520..44664dcf 100644 --- a/src/com/android/tv/data/PreviewDataManager.java +++ b/src/com/android/tv/data/PreviewDataManager.java @@ -35,10 +35,8 @@ import android.support.media.tv.ChannelLogoUtils; import android.support.media.tv.PreviewProgram; import android.util.Log; import android.util.Pair; - import com.android.tv.R; -import com.android.tv.util.PermissionUtils; - +import com.android.tv.common.util.PermissionUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashMap; @@ -46,32 +44,24 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -/** - * Class to manage the preview data. - */ +/** Class to manage the preview data. */ @TargetApi(Build.VERSION_CODES.O) @MainThread public class PreviewDataManager { private static final String TAG = "PreviewDataManager"; - // STOPSHIP: set it to false. - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; - /** - * Invalid preview channel ID. - */ + /** Invalid preview channel ID. */ public static final long INVALID_PREVIEW_CHANNEL_ID = -1; + @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL}) @Retention(RetentionPolicy.SOURCE) - public @interface PreviewChannelType{} + public @interface PreviewChannelType {} - /** - * Type of default preview channel - */ - public static final long TYPE_DEFAULT_PREVIEW_CHANNEL = 1; - /** - * Type of recorded program channel - */ - public static final long TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2; + /** Type of default preview channel */ + public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1; + /** Type of recorded program channel */ + public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2; private final Context mContext; private final ContentResolver mContentResolver; @@ -80,8 +70,7 @@ public class PreviewDataManager { private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>(); private QueryPreviewDataTask mQueryPreviewTask; - private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = - new HashMap<>(); + private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = new HashMap<>(); private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>(); private final int mPreviewChannelLogoWidth; @@ -90,15 +79,13 @@ public class PreviewDataManager { public PreviewDataManager(Context context) { mContext = context.getApplicationContext(); mContentResolver = context.getContentResolver(); - mPreviewChannelLogoWidth = mContext.getResources().getDimensionPixelSize( - R.dimen.preview_channel_logo_width); - mPreviewChannelLogoHeight = mContext.getResources().getDimensionPixelSize( - R.dimen.preview_channel_logo_height); + mPreviewChannelLogoWidth = + mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_width); + mPreviewChannelLogoHeight = + mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_height); } - /** - * Starts the preview data manager. - */ + /** Starts the preview data manager. */ public void start() { if (mQueryPreviewTask == null) { mQueryPreviewTask = new QueryPreviewDataTask(); @@ -106,19 +93,17 @@ public class PreviewDataManager { } } - /** - * Stops the preview data manager. - */ + /** Stops the preview data manager. */ public void stop() { if (mQueryPreviewTask != null) { mQueryPreviewTask.cancel(true); } - for (CreatePreviewChannelTask createPreviewChannelTask - : mCreatePreviewChannelTasks.values()) { + for (CreatePreviewChannelTask createPreviewChannelTask : + mCreatePreviewChannelTasks.values()) { createPreviewChannelTask.cancel(true); } - for (UpdatePreviewProgramTask updatePreviewProgramTask - : mUpdatePreviewProgramTasks.values()) { + for (UpdatePreviewProgramTask updatePreviewProgramTask : + mUpdatePreviewProgramTasks.values()) { updatePreviewProgramTask.cancel(true); } @@ -127,31 +112,26 @@ public class PreviewDataManager { mUpdatePreviewProgramTasks.clear(); } - /** - * Gets preview channel ID from the preview channel type. - */ + /** Gets preview channel ID from the preview channel type. */ public @PreviewChannelType long getPreviewChannelId(long previewChannelType) { return mPreviewData.getPreviewChannelId(previewChannelType); } - /** - * Creates default preview channel. - */ + /** Creates default preview channel. */ public void createDefaultPreviewChannel( OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) { createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener); } - /** - * Creates a preview channel for specific channel type. - */ - public void createPreviewChannel(@PreviewChannelType long previewChannelType, + /** Creates a preview channel for specific channel type. */ + public void createPreviewChannel( + @PreviewChannelType long previewChannelType, OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) { CreatePreviewChannelTask currentRunningCreateTask = mCreatePreviewChannelTasks.get(previewChannelType); if (currentRunningCreateTask == null) { - CreatePreviewChannelTask createPreviewChannelTask = new CreatePreviewChannelTask( - previewChannelType); + CreatePreviewChannelTask createPreviewChannelTask = + new CreatePreviewChannelTask(previewChannelType); createPreviewChannelTask.addOnPreviewChannelCreationResultListener( onPreviewChannelCreationResultListener); createPreviewChannelTask.execute(); @@ -162,32 +142,26 @@ public class PreviewDataManager { } } - /** - * Returns {@code true} if the preview data is loaded. - */ + /** Returns {@code true} if the preview data is loaded. */ public boolean isLoadFinished() { return mLoadFinished; } - /** - * Adds listener. - */ + /** Adds listener. */ public void addListener(PreviewDataListener previewDataListener) { mPreviewDataListeners.add(previewDataListener); } - /** - * Removes listener. - */ + /** Removes listener. */ public void removeListener(PreviewDataListener previewDataListener) { mPreviewDataListeners.remove(previewDataListener); } - /** - * Updates the preview programs table for a specific preview channel. - */ - public void updatePreviewProgramsForChannel(long previewChannelId, - Set<PreviewProgramContent> programs, PreviewDataListener previewDataListener) { + /** Updates the preview programs table for a specific preview channel. */ + public void updatePreviewProgramsForChannel( + long previewChannelId, + Set<PreviewProgramContent> programs, + PreviewDataListener previewDataListener) { UpdatePreviewProgramTask currentRunningUpdateTask = mUpdatePreviewProgramTasks.get(previewChannelId); if (currentRunningUpdateTask != null @@ -215,22 +189,19 @@ public class PreviewDataManager { } public interface PreviewDataListener { - /** - * Called when the preview data is loaded. - */ + /** Called when the preview data is loaded. */ void onPreviewDataLoadFinished(); - /** - * Called when the preview data is updated. - */ + /** Called when the preview data is updated. */ void onPreviewDataUpdateFinished(); } public interface OnPreviewChannelCreationResultListener { /** * Called when the creation of preview channel is finished. - * @param createdPreviewChannelId The preview channel ID if created successfully, - * otherwise it's {@value #INVALID_PREVIEW_CHANNEL_ID}. + * + * @param createdPreviewChannelId The preview channel ID if created successfully, otherwise + * it's {@value #INVALID_PREVIEW_CHANNEL_ID}. */ void onPreviewChannelCreationResult(long createdPreviewChannelId); } @@ -283,7 +254,7 @@ public class PreviewDataManager { android.support.media.tv.Channel previewChannel = android.support.media.tv.Channel.fromCursor(cursor); Long previewChannelType = previewChannel.getInternalProviderFlag1(); - if (previewChannel.getPackageName() == packageName + if (packageName.equals(previewChannel.getPackageName()) && previewChannelType != null) { previewData.addPreviewChannelId( previewChannelType, previewChannel.getId()); @@ -352,9 +323,11 @@ public class PreviewDataManager { if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground"); long previewChannelId; try { - Uri channelUri = mContentResolver.insert(TvContract.Channels.CONTENT_URI, - PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType) - .toContentValues()); + Uri channelUri = + mContentResolver.insert( + TvContract.Channels.CONTENT_URI, + PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType) + .toContentValues()); if (channelUri != null) { previewChannelId = ContentUris.parseId(channelUri); } else { @@ -367,9 +340,14 @@ public class PreviewDataManager { } Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager()); if (appIcon != null && appIcon instanceof BitmapDrawable) { - ChannelLogoUtils.storeChannelLogo(mContext, previewChannelId, - Bitmap.createScaledBitmap(((BitmapDrawable) appIcon).getBitmap(), - mPreviewChannelLogoWidth, mPreviewChannelLogoHeight, false)); + ChannelLogoUtils.storeChannelLogo( + mContext, + previewChannelId, + Bitmap.createScaledBitmap( + ((BitmapDrawable) appIcon).getBitmap(), + mPreviewChannelLogoWidth, + mPreviewChannelLogoHeight, + false)); } return previewChannelId; } @@ -380,8 +358,8 @@ public class PreviewDataManager { if (result != INVALID_PREVIEW_CHANNEL_ID) { mPreviewData.addPreviewChannelId(mPreviewChannelType, result); } - for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener - : mOnPreviewChannelCreationResultListeners) { + for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener : + mOnPreviewChannelCreationResultListeners) { onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result); } mCreatePreviewChannelTasks.remove(mPreviewChannelType); @@ -389,8 +367,8 @@ public class PreviewDataManager { } /** - * Updates the whole data which belongs to the package in preview programs table for a - * specific preview channel with a set of {@link PreviewProgramContent}. + * Updates the whole data which belongs to the package in preview programs table for a specific + * preview channel with a set of {@link PreviewProgramContent}. */ private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> { private long mPreviewChannelId; @@ -398,15 +376,15 @@ public class PreviewDataManager { private Map<Long, Long> mCurrentProgramId2PreviewProgramId; private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>(); - public UpdatePreviewProgramTask(long previewChannelId, - Set<PreviewProgramContent> programs) { + public UpdatePreviewProgramTask( + long previewChannelId, Set<PreviewProgramContent> programs) { mPreviewChannelId = previewChannelId; mPrograms = programs; if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) { mCurrentProgramId2PreviewProgramId = new HashMap<>(); } else { - mCurrentProgramId2PreviewProgramId = new HashMap<>( - mPreviewData.getPreviewProgramIds(previewChannelId)); + mCurrentProgramId2PreviewProgramId = + new HashMap<>(mPreviewData.getPreviewProgramIds(previewChannelId)); } } @@ -440,14 +418,22 @@ public class PreviewDataManager { } Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId()); if (existingPreviewProgramId != null) { - if (DEBUG) Log.d(TAG, "Preview program " + existingPreviewProgramId + " " + - "already exists for program " + program.getId()); + if (DEBUG) + Log.d( + TAG, + "Preview program " + + existingPreviewProgramId + + " " + + "already exists for program " + + program.getId()); continue; } try { - Uri programUri = mContentResolver.insert(TvContract.PreviewPrograms.CONTENT_URI, - PreviewDataUtils.createPreviewProgramFromContent(program) - .toContentValues()); + Uri programUri = + mContentResolver.insert( + TvContract.PreviewPrograms.CONTENT_URI, + PreviewDataUtils.createPreviewProgramFromContent(program) + .toContentValues()); if (programUri != null) { long previewProgramId = ContentUris.parseId(programUri); mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId); @@ -466,8 +452,10 @@ public class PreviewDataManager { } try { if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key)); - mContentResolver.delete(TvContract.buildPreviewProgramUri( - uncheckedPrograms.get(key)), null, null); + mContentResolver.delete( + TvContract.buildPreviewProgramUri(uncheckedPrograms.get(key)), + null, + null); mCurrentProgramId2PreviewProgramId.remove(key); } catch (Exception e) { Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key)); @@ -493,9 +481,7 @@ public class PreviewDataManager { } } - /** - * Class to store the query result of preview data. - */ + /** Class to store the query result of preview data. */ private static final class PreviewData { private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>(); private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>(); @@ -565,13 +551,9 @@ public class PreviewDataManager { } } - /** - * A utils class for preview data. - */ - public final static class PreviewDataUtils { - /** - * Creates a preview channel. - */ + /** A utils class for preview data. */ + public static final class PreviewDataUtils { + /** Creates a preview channel. */ public static android.support.media.tv.Channel createPreviewChannel( Context context, @PreviewChannelType long previewChannelType) { if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) { @@ -590,7 +572,7 @@ public class PreviewDataManager { context.getApplicationInfo().loadDescription(context.getPackageManager()); builder.setType(TvContract.Channels.TYPE_PREVIEW) .setDisplayName(appLabel == null ? null : appLabel.toString()) - .setDescription(appDescription == null ? null : appDescription.toString()) + .setDescription(appDescription == null ? null : appDescription.toString()) .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI) .setInternalProviderFlag1(previewChannelType); return builder.build(); @@ -601,16 +583,15 @@ public class PreviewDataManager { android.support.media.tv.Channel.Builder builder = new android.support.media.tv.Channel.Builder(); builder.setType(TvContract.Channels.TYPE_PREVIEW) - .setDisplayName(context.getResources().getString( - R.string.recorded_programs_preview_channel)) + .setDisplayName( + context.getResources() + .getString(R.string.recorded_programs_preview_channel)) .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI) .setInternalProviderFlag1(previewChannelType); return builder.build(); } - /** - * Creates a preview program. - */ + /** Creates a preview program. */ public static PreviewProgram createPreviewProgramFromContent( PreviewProgramContent program) { PreviewProgram.Builder builder = new PreviewProgram.Builder(); @@ -622,13 +603,12 @@ public class PreviewDataManager { .setPosterArtUri(program.getPosterArtUri()) .setIntentUri(program.getIntentUri()) .setPreviewVideoUri(program.getPreviewVideoUri()) - .setInternalProviderId(Long.toString(program.getId())); + .setInternalProviderId(Long.toString(program.getId())) + .setContentId(program.getIntentUri().toString()); return builder.build(); } - /** - * Appends query parameters to a Uri. - */ + /** Appends query parameters to a Uri. */ public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) { return uri.buildUpon().appendQueryParameter(param.first, param.second).build(); } diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java index 39f5051d..b5156408 100644 --- a/src/com/android/tv/data/PreviewProgramContent.java +++ b/src/com/android/tv/data/PreviewProgramContent.java @@ -17,21 +17,19 @@ package com.android.tv.data; import android.content.Context; -import android.media.tv.TvContract; import android.net.Uri; +import android.support.annotation.VisibleForTesting; +import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Pair; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; - import java.util.Objects; -/** - * A class to store the content of preview programs. - */ +/** A class to store the content of preview programs. */ public class PreviewProgramContent { - private final static String PARAM_INPUT = "input"; + @VisibleForTesting static final String PARAM_INPUT = "input"; private long mId; private long mPreviewChannelId; @@ -43,59 +41,71 @@ public class PreviewProgramContent { private Uri mIntentUri; private Uri mPreviewVideoUri; - /** - * Create preview program content from {@link Program} - */ - public static PreviewProgramContent createFromProgram(Context context, - long previewChannelId, Program program) { - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(program.getChannelId()); - if (channel == null) { - return null; - } + /** Create preview program content from {@link Program} */ + public static PreviewProgramContent createFromProgram( + Context context, long previewChannelId, Program program) { + Channel channel = + TvSingletons.getSingletons(context) + .getChannelDataManager() + .getChannel(program.getChannelId()); + return channel == null ? null : createFromProgram(previewChannelId, program, channel); + } + + /** Create preview program content from {@link RecordedProgram} */ + public static PreviewProgramContent createFromRecordedProgram( + Context context, long previewChannelId, RecordedProgram recordedProgram) { + Channel channel = + TvSingletons.getSingletons(context) + .getChannelDataManager() + .getChannel(recordedProgram.getChannelId()); + return createFromRecordedProgram(previewChannelId, recordedProgram, channel); + } + + @VisibleForTesting + static PreviewProgramContent createFromProgram( + long previewChannelId, Program program, Channel channel) { String channelDisplayName = channel.getDisplayName(); return new PreviewProgramContent.Builder() .setId(program.getId()) .setPreviewChannelId(previewChannelId) - .setType(TvContract.PreviewPrograms.TYPE_CHANNEL) + .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL) .setLive(true) .setTitle(program.getTitle()) - .setDescription(!TextUtils.isEmpty(channelDisplayName) - ? channelDisplayName : channel.getDisplayNumber()) + .setDescription( + !TextUtils.isEmpty(channelDisplayName) + ? channelDisplayName + : channel.getDisplayNumber()) .setPosterArtUri(Uri.parse(program.getPosterArtUri())) .setIntentUri(channel.getUri()) - .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri( - channel.getUri(), new Pair<>(PARAM_INPUT, channel.getInputId()))) + .setPreviewVideoUri( + PreviewDataManager.PreviewDataUtils.addQueryParamToUri( + channel.getUri(), new Pair<>(PARAM_INPUT, channel.getInputId()))) .build(); } - /** - * Create preview program content from {@link RecordedProgram} - */ - public static PreviewProgramContent createFromRecordedProgram( - Context context, long previewChannelId, RecordedProgram recordedProgram) { - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(recordedProgram.getChannelId()); - String channelDisplayName = null; - if (channel != null) { - channelDisplayName = channel.getDisplayName(); - } - Uri recordedProgramUri = TvContract.buildRecordedProgramUri(recordedProgram.getId()); + @VisibleForTesting + static PreviewProgramContent createFromRecordedProgram( + long previewChannelId, RecordedProgram recordedProgram, Channel channel) { + String channelDisplayName = channel == null ? null : channel.getDisplayName(); + Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(recordedProgram.getId()); return new PreviewProgramContent.Builder() .setId(recordedProgram.getId()) .setPreviewChannelId(previewChannelId) - .setType(TvContract.PreviewPrograms.TYPE_CLIP) + .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) .setTitle(recordedProgram.getTitle()) .setDescription(channelDisplayName != null ? channelDisplayName : "") .setPosterArtUri(Uri.parse(recordedProgram.getPosterArtUri())) .setIntentUri(recordedProgramUri) - .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri( - recordedProgramUri, new Pair<>(PARAM_INPUT, recordedProgram.getInputId()))) + .setPreviewVideoUri( + PreviewDataManager.PreviewDataUtils.addQueryParamToUri( + recordedProgramUri, + new Pair<>(PARAM_INPUT, recordedProgram.getInputId()))) .build(); } - private PreviewProgramContent() { } + private PreviewProgramContent() {} + @SuppressWarnings("ReferenceEquality") public void copyFrom(PreviewProgramContent other) { if (this == other) { return; @@ -119,58 +129,42 @@ public class PreviewProgramContent { return mId; } - /** - * Returns the preview channel id which the preview program belongs to. - */ + /** Returns the preview channel id which the preview program belongs to. */ public long getPreviewChannelId() { return mPreviewChannelId; } - /** - * Returns the type of the preview program. - */ + /** Returns the type of the preview program. */ public int getType() { return mType; } - /** - * Returns whether the preview program is live or not. - */ + /** Returns whether the preview program is live or not. */ public boolean getLive() { return mLive; } - /** - * Returns the title of the preview program. - */ + /** Returns the title of the preview program. */ public String getTitle() { return mTitle; } - /** - * Returns the description of the preview program. - */ + /** Returns the description of the preview program. */ public String getDescription() { return mDescription; } - /** - * Returns the poster art uri of the preview program. - */ + /** Returns the poster art uri of the preview program. */ public Uri getPosterArtUri() { return mPosterArtUri; } - /** - * Returns the intent uri of the preview program. - */ + /** Returns the intent uri of the preview program. */ public Uri getIntentUri() { return mIntentUri; } - /** - * Returns the preview video uri of the preview program. - */ + /** Returns the preview video uri of the preview program. */ public Uri getPreviewVideoUri() { return mPreviewVideoUri; } @@ -194,8 +188,16 @@ public class PreviewProgramContent { @Override public int hashCode() { - return Objects.hash(mId, mPreviewChannelId, mType, mLive, mTitle, mDescription, - mPosterArtUri, mIntentUri, mPreviewVideoUri); + return Objects.hash( + mId, + mPreviewChannelId, + mType, + mLive, + mTitle, + mDescription, + mPosterArtUri, + mIntentUri, + mPreviewVideoUri); } public static final class Builder { diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 071c7024..2c64cdbb 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -32,59 +32,56 @@ import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; - import com.android.tv.common.BuildConfig; -import com.android.tv.common.CollectionUtils; import com.android.tv.common.TvContentRatingCache; -import com.android.tv.util.ImageLoader; +import com.android.tv.common.util.CollectionUtils; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.data.api.Channel; import com.android.tv.util.Utils; - +import com.android.tv.util.images.ImageLoader; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; -/** - * A convenience class to create and insert program information entries into the database. - */ +/** A convenience class to create and insert program information entries into the database. */ public final class Program extends BaseProgram implements Comparable<Program>, Parcelable { private static final boolean DEBUG = false; private static final boolean DEBUG_DUMP_DESCRIPTION = false; private static final String TAG = "Program"; private static final String[] PROJECTION_BASE = { - // Columns must match what is read in Program.fromCursor() - TvContract.Programs._ID, - TvContract.Programs.COLUMN_PACKAGE_NAME, - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_EPISODE_TITLE, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_LONG_DESCRIPTION, - TvContract.Programs.COLUMN_POSTER_ART_URI, - TvContract.Programs.COLUMN_THUMBNAIL_URI, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_VIDEO_WIDTH, - TvContract.Programs.COLUMN_VIDEO_HEIGHT, - TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA + // Columns must match what is read in Program.fromCursor() + TvContract.Programs._ID, + TvContract.Programs.COLUMN_PACKAGE_NAME, + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.Programs.COLUMN_TITLE, + TvContract.Programs.COLUMN_EPISODE_TITLE, + TvContract.Programs.COLUMN_SHORT_DESCRIPTION, + TvContract.Programs.COLUMN_LONG_DESCRIPTION, + TvContract.Programs.COLUMN_POSTER_ART_URI, + TvContract.Programs.COLUMN_THUMBNAIL_URI, + TvContract.Programs.COLUMN_CANONICAL_GENRE, + TvContract.Programs.COLUMN_CONTENT_RATING, + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_VIDEO_WIDTH, + TvContract.Programs.COLUMN_VIDEO_HEIGHT, + TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA }; // Columns which is deprecated in NYC @SuppressWarnings("deprecation") private static final String[] PROJECTION_DEPRECATED_IN_NYC = { - TvContract.Programs.COLUMN_SEASON_NUMBER, - TvContract.Programs.COLUMN_EPISODE_NUMBER + TvContract.Programs.COLUMN_SEASON_NUMBER, TvContract.Programs.COLUMN_EPISODE_NUMBER }; private static final String[] PROJECTION_ADDED_IN_NYC = { - TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_SEASON_TITLE, - TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_RECORDING_PROHIBITED + TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, + TvContract.Programs.COLUMN_SEASON_TITLE, + TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, + TvContract.Programs.COLUMN_RECORDING_PROHIBITED }; public static final String[] PROJECTION = createProjection(); @@ -97,9 +94,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P : PROJECTION_DEPRECATED_IN_NYC); } - /** - * Returns the column index for {@code column}, -1 if the column doesn't exist. - */ + /** Returns the column index for {@code column}, -1 if the column doesn't exist. */ public static int getColumnIndex(String column) { for (int i = 0; i < PROJECTION.length; ++i) { if (PROJECTION[i].equals(column)) { @@ -135,7 +130,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P builder.setEndTimeUtcMillis(cursor.getLong(index++)); builder.setVideoWidth((int) cursor.getLong(index++)); builder.setVideoHeight((int) cursor.getLong(index++)); - if (Utils.isInBundledPackageSet(packageName)) { + if (CommonUtils.isInBundledPackageSet(packageName)) { InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); } index++; @@ -183,17 +178,18 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return program; } - public static final Parcelable.Creator<Program> CREATOR = new Parcelable.Creator<Program>() { - @Override - public Program createFromParcel(Parcel in) { - return Program.fromParcel(in); - } + public static final Parcelable.Creator<Program> CREATOR = + new Parcelable.Creator<Program>() { + @Override + public Program createFromParcel(Parcel in) { + return Program.fromParcel(in); + } - @Override - public Program[] newArray(int size) { - return new Program[size]; - } - }; + @Override + public Program[] newArray(int size) { + return new Program[size]; + } + }; private long mId; private String mPackageName; @@ -225,9 +221,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mId; } - /** - * Returns the package name of this program. - */ + /** Returns the package name of this program. */ public String getPackageName() { return mPackageName; } @@ -236,18 +230,14 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mChannelId; } - /** - * Returns {@code true} if this program is valid or {@code false} otherwise. - */ + /** Returns {@code true} if this program is valid or {@code false} otherwise. */ @Override public boolean isValid() { return mChannelId >= 0; } - /** - * Returns {@code true} if the program is valid and {@code false} otherwise. - */ - public static boolean isValid(Program program) { + /** Returns {@code true} if the program is valid and {@code false} otherwise. */ + public static boolean isProgramValid(Program program) { return program != null && program.isValid(); } @@ -256,17 +246,13 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mTitle; } - /** - * Returns the series ID. - */ + /** Returns the series ID. */ @Override public String getSeriesId() { return mSeriesId; } - /** - * Returns the episode title. - */ + /** Returns the episode title. */ @Override public String getEpisodeTitle() { return mEpisodeTitle; @@ -292,9 +278,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mEndTimeUtcMillis; } - /** - * Returns the program duration. - */ + /** Returns the program duration. */ @Override public long getDurationMillis() { return mEndTimeUtcMillis - mStartTimeUtcMillis; @@ -318,9 +302,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mVideoHeight; } - /** - * Returns the list of Critic Scores for this program - */ + /** Returns the list of Critic Scores for this program */ @Nullable public List<CriticScore> getCriticScores() { return mCriticScores; @@ -342,17 +324,12 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return mThumbnailUri; } - /** - * Returns {@code true} if the recording of this program is prohibited. - */ + /** Returns {@code true} if the recording of this program is prohibited. */ public boolean isRecordingProhibited() { return mRecordingProhibited; } - /** - * Returns array of canonical genres for this program. - * This is expected to be called rarely. - */ + /** Returns array of canonical genres for this program. This is expected to be called rarely. */ @Nullable public String[] getCanonicalGenres() { if (mCanonicalGenreIds == null) { @@ -365,17 +342,13 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return genres; } - /** - * Returns array of canonical genre ID's for this program. - */ + /** Returns array of canonical genre ID's for this program. */ @Override public int[] getCanonicalGenreIds() { return mCanonicalGenreIds; } - /** - * Returns if this program has the genre. - */ + /** Returns if this program has the genre. */ public boolean hasGenre(int genreId) { if (genreId == GenreItems.ID_ALL_CHANNELS) { return true; @@ -393,10 +366,24 @@ public final class Program extends BaseProgram implements Comparable<Program>, P @Override public int hashCode() { // Hash with all the properties because program ID can be invalid for the dummy programs. - return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, - mTitle, mSeriesId, mEpisodeTitle, mDescription, mLongDescription, mVideoWidth, - mVideoHeight, mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings), - Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber, + return Objects.hash( + mChannelId, + mStartTimeUtcMillis, + mEndTimeUtcMillis, + mTitle, + mSeriesId, + mEpisodeTitle, + mDescription, + mLongDescription, + mVideoWidth, + mVideoHeight, + mPosterArtUri, + mThumbnailUri, + Arrays.hashCode(mContentRatings), + Arrays.hashCode(mCanonicalGenreIds), + mSeasonNumber, + mSeasonTitle, + mEpisodeNumber, mRecordingProhibited); } @@ -436,28 +423,47 @@ public final class Program extends BaseProgram implements Comparable<Program>, P @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("Program[").append(mId) - .append("]{channelId=").append(mChannelId) - .append(", packageName=").append(mPackageName) - .append(", title=").append(mTitle) - .append(", seriesId=").append(mSeriesId) - .append(", episodeTitle=").append(mEpisodeTitle) - .append(", seasonNumber=").append(mSeasonNumber) - .append(", seasonTitle=").append(mSeasonTitle) - .append(", episodeNumber=").append(mEpisodeNumber) - .append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis)) - .append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis)) - .append(", videoWidth=").append(mVideoWidth) - .append(", videoHeight=").append(mVideoHeight) + builder.append("Program[") + .append(mId) + .append("]{channelId=") + .append(mChannelId) + .append(", packageName=") + .append(mPackageName) + .append(", title=") + .append(mTitle) + .append(", seriesId=") + .append(mSeriesId) + .append(", episodeTitle=") + .append(mEpisodeTitle) + .append(", seasonNumber=") + .append(mSeasonNumber) + .append(", seasonTitle=") + .append(mSeasonTitle) + .append(", episodeNumber=") + .append(mEpisodeNumber) + .append(", startTimeUtcSec=") + .append(Utils.toTimeString(mStartTimeUtcMillis)) + .append(", endTimeUtcSec=") + .append(Utils.toTimeString(mEndTimeUtcMillis)) + .append(", videoWidth=") + .append(mVideoWidth) + .append(", videoHeight=") + .append(mVideoHeight) .append(", contentRatings=") .append(TvContentRatingCache.contentRatingsToString(mContentRatings)) - .append(", posterArtUri=").append(mPosterArtUri) - .append(", thumbnailUri=").append(mThumbnailUri) - .append(", canonicalGenres=").append(Arrays.toString(mCanonicalGenreIds)) - .append(", recordingProhibited=").append(mRecordingProhibited); + .append(", posterArtUri=") + .append(mPosterArtUri) + .append(", thumbnailUri=") + .append(mThumbnailUri) + .append(", canonicalGenres=") + .append(Arrays.toString(mCanonicalGenreIds)) + .append(", recordingProhibited=") + .append(mRecordingProhibited); if (DEBUG_DUMP_DESCRIPTION) { - builder.append(", description=").append(mDescription) - .append(", longDescription=").append(mLongDescription); + builder.append(", description=") + .append(mDescription) + .append(", longDescription=") + .append(mLongDescription); } return builder.append("}").toString(); } @@ -471,12 +477,19 @@ public final class Program extends BaseProgram implements Comparable<Program>, P public static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); + if (!TextUtils.isEmpty(program.getPackageName())) { + values.put(Programs.COLUMN_PACKAGE_NAME, program.getPackageName()); + } putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, + putValue( + values, + TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, + putValue( + values, + TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber()); } else { putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); @@ -488,17 +501,23 @@ public final class Program extends BaseProgram implements Comparable<Program>, P putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); String[] canonicalGenres = program.getCanonicalGenres(); if (canonicalGenres != null && canonicalGenres.length > 0) { - putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, + putValue( + values, + TvContract.Programs.COLUMN_CANONICAL_GENRE, TvContract.Programs.Genres.encode(canonicalGenres)); } else { putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); } - putValue(values, Programs.COLUMN_CONTENT_RATING, + putValue( + values, + Programs.COLUMN_CONTENT_RATING, TvContentRatingCache.contentRatingsToString(program.getContentRatings())); - values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - program.getStartTimeUtcMillis()); + values.put( + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); - putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, + putValue( + values, + TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(program)); return values; } @@ -547,15 +566,11 @@ public final class Program extends BaseProgram implements Comparable<Program>, P mRecordingProhibited = other.mRecordingProhibited; } - /** - * A Builder for the Program class - */ + /** A Builder for the Program class */ public static final class Builder { private final Program mProgram; - /** - * Creates a Builder for this Program class - */ + /** Creates a Builder for this Program class */ public Builder() { mProgram = new Program(); // Fill initial data. @@ -574,8 +589,9 @@ public final class Program extends BaseProgram implements Comparable<Program>, P } /** - * Creates a builder for this Program class - * by setting default values equivalent to another Program + * Creates a builder for this Program class by setting default values equivalent to another + * Program + * * @param other the program to be copied */ @VisibleForTesting @@ -586,6 +602,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the ID of this program + * * @param id the ID * @return a reference to this object */ @@ -596,16 +613,18 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the package name for this program + * * @param packageName the package name * @return a reference to this object */ - public Builder setPackageName(String packageName){ + public Builder setPackageName(String packageName) { mProgram.mPackageName = packageName; return this; } /** * Sets the channel ID for this program + * * @param channelId the channel ID * @return a reference to this object */ @@ -616,6 +635,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the program title + * * @param title the title * @return a reference to this object */ @@ -626,6 +646,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the series ID. + * * @param seriesId the series ID * @return a reference to this object */ @@ -636,6 +657,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the episode title if this is a series program + * * @param episodeTitle the episode title * @return a reference to this object */ @@ -646,6 +668,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the season number if this is a series program + * * @param seasonNumber the season number * @return a reference to this object */ @@ -654,9 +677,9 @@ public final class Program extends BaseProgram implements Comparable<Program>, P return this; } - /** * Sets the season title if this is a series program + * * @param seasonTitle the season title * @return a reference to this object */ @@ -667,6 +690,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the episode number if this is a series program + * * @param episodeNumber the episode number * @return a reference to this object */ @@ -677,6 +701,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the start time of this program + * * @param startTimeUtcMillis the start time in UTC milliseconds * @return a reference to this object */ @@ -687,6 +712,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the end time of this program + * * @param endTimeUtcMillis the end time in UTC milliseconds * @return a reference to this object */ @@ -697,6 +723,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets a description + * * @param description the description * @return a reference to this object */ @@ -707,6 +734,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets a long description + * * @param longDescription the long description * @return a reference to this object */ @@ -717,6 +745,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Defines the video width of this program + * * @param width * @return a reference to this object */ @@ -727,6 +756,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Defines the video height of this program + * * @param height * @return a reference to this object */ @@ -737,6 +767,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the content ratings for this program + * * @param contentRatings the content ratings * @return a reference to this object */ @@ -747,6 +778,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the poster art URI + * * @param posterArtUri the poster art URI * @return a reference to this object */ @@ -757,6 +789,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the thumbnail URI + * * @param thumbnailUri the thumbnail URI * @return a reference to this object */ @@ -767,6 +800,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the canonical genres by id + * * @param genres the genres * @return a reference to this object */ @@ -777,6 +811,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the recording prohibited flag + * * @param recordingProhibited recording prohibited flag * @return a reference to this object */ @@ -787,6 +822,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Adds a critic score + * * @param criticScore the critic score * @return a reference to this object */ @@ -802,6 +838,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Sets the critic scores + * * @param criticScores the critic scores * @return a reference to this objects */ @@ -812,6 +849,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Returns a reference to the Program object being constructed + * * @return the Program object constructed */ public Program build() { @@ -831,7 +869,9 @@ public final class Program extends BaseProgram implements Comparable<Program>, P } /** - * Prefetches the program poster art.<p> + * Prefetches the program poster art. + * + * <p> */ public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) { if (mPosterArtUri == null) { @@ -842,13 +882,17 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Loads the program poster art and returns it via {@code callback}. - * <p> - * Note that it may directly call {@code callback} if the program poster art already is loaded. + * + * <p>Note that it may directly call {@code callback} if the program poster art already is + * loaded. * * @return {@code true} if the load is complete and the callback is executed. */ @UiThread - public boolean loadPosterArt(Context context, int posterArtWidth, int posterArtHeight, + public boolean loadPosterArt( + Context context, + int posterArtWidth, + int posterArtHeight, ImageLoader.ImageLoaderCallback callback) { if (mPosterArtUri == null) { return false; @@ -861,12 +905,18 @@ public final class Program extends BaseProgram implements Comparable<Program>, P if (p1 == null || p2 == null) { return false; } - boolean isDuplicate = p1.getChannelId() == p2.getChannelId() - && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() - && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); + boolean isDuplicate = + p1.getChannelId() == p2.getChannelId() + && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() + && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); if (DEBUG && BuildConfig.ENG && isDuplicate) { - Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \"" - + p2.getTitle() + "\""); + Log.w( + TAG, + "Duplicate programs detected! - \"" + + p1.getTitle() + + "\" and \"" + + p2.getTitle() + + "\""); } return isDuplicate; } @@ -906,21 +956,13 @@ public final class Program extends BaseProgram implements Comparable<Program>, P out.writeByte((byte) (mRecordingProhibited ? 1 : 0)); } - /** - * Holds one type of critic score and its source. - */ + /** Holds one type of critic score and its source. */ public static final class CriticScore implements Serializable, Parcelable { - /** - * The source of the rating. - */ + /** The source of the rating. */ public final String source; - /** - * The score. - */ + /** The score. */ public final String score; - /** - * The url of the logo image - */ + /** The url of the logo image */ public final String logoUrl; public static final Parcelable.Creator<CriticScore> CREATOR = @@ -929,7 +971,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P public CriticScore createFromParcel(Parcel in) { String source = in.readString(); String score = in.readString(); - String logoUri = in.readString(); + String logoUri = in.readString(); return new CriticScore(source, score, logoUri); } @@ -941,6 +983,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Constructor for this class. + * * @param source the source of the rating * @param score the score */ diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index 8cb5e74a..4631806c 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -33,14 +33,16 @@ import android.util.ArraySet; import android.util.Log; import android.util.LongSparseArray; import android.util.LruCache; - -import com.android.tv.common.MemoryManageable; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.config.api.RemoteConfig; +import com.android.tv.common.config.api.RemoteConfigValue; +import com.android.tv.common.memory.MemoryManageable; +import com.android.tv.common.util.Clock; +import com.android.tv.data.api.Channel; import com.android.tv.util.AsyncDbTask; -import com.android.tv.util.Clock; import com.android.tv.util.MultiLongSparseArray; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -51,6 +53,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @MainThread @@ -60,23 +63,28 @@ public class ProgramDataManager implements MemoryManageable { // To prevent from too many program update operations at the same time, we give random interval // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS. - private static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5); + @VisibleForTesting + static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5); + private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10); private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); // TODO: need to optimize consecutive DB updates. private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5); - @VisibleForTesting - static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30); - @VisibleForTesting - static final long PROGRAM_GUIDE_MAX_TIME_RANGE = TimeUnit.DAYS.toMillis(2); + @VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30); + private static final RemoteConfigValue<Long> PROGRAM_GUIDE_MAX_HOURS = + RemoteConfigValue.create("live_channels_program_guide_max_hours", 48); // TODO: Use TvContract constants, once they become public. private static final String PARAM_START_TIME = "start_time"; private static final String PARAM_END_TIME = "end_time"; // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs. // Duplicated programs are always consecutive by the sorting order. - private static final String SORT_BY_TIME = Programs.COLUMN_START_TIME_UTC_MILLIS + ", " - + Programs.COLUMN_CHANNEL_ID + ", " + Programs.COLUMN_END_TIME_UTC_MILLIS; + private static final String SORT_BY_TIME = + Programs.COLUMN_START_TIME_UTC_MILLIS + + ", " + + Programs.COLUMN_CHANNEL_ID + + ", " + + Programs.COLUMN_END_TIME_UTC_MILLIS; private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000; private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001; @@ -84,6 +92,8 @@ public class ProgramDataManager implements MemoryManageable { private final Clock mClock; private final ContentResolver mContentResolver; + private final Executor mDbExecutor; + private final RemoteConfig mRemoteConfig; private boolean mStarted; // Updated only on the main thread. private volatile boolean mCurrentProgramsLoadFinished; @@ -114,32 +124,47 @@ public class ProgramDataManager implements MemoryManageable { @MainThread public ProgramDataManager(Context context) { - this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper()); + this( + TvSingletons.getSingletons(context).getDbExecutor(), + context.getContentResolver(), + Clock.SYSTEM, + Looper.myLooper(), + TvSingletons.getSingletons(context).getRemoteConfig()); } @VisibleForTesting - ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) { + ProgramDataManager( + Executor executor, + ContentResolver contentResolver, + Clock time, + Looper looper, + RemoteConfig remoteConfig) { + mDbExecutor = executor; mClock = time; mContentResolver = contentResolver; mHandler = new MyHandler(looper); - mProgramObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { - mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); - } - if (isProgramUpdatePaused()) { - return; - } - if (mPrefetchEnabled) { - // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long - // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message - // and send MSG_UPDATE_PREFETCH_PROGRAM again. - mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); - mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); - } - } - }; + mRemoteConfig = remoteConfig; + mProgramObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); + } + if (isProgramUpdatePaused()) { + return; + } + if (mPrefetchEnabled) { + // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be + // quite long + // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing + // message + // and send MSG_UPDATE_PREFETCH_PROGRAM again. + mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } + } + }; mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS; } @@ -149,18 +174,16 @@ public class ProgramDataManager implements MemoryManageable { } /** - * Set the program prefetch update wait which gives the delay to query all programs from DB - * to prevent from too frequent DB queries. - * Default value is {@link #PROGRAM_PREFETCH_UPDATE_WAIT_MS} + * Set the program prefetch update wait which gives the delay to query all programs from DB to + * prevent from too frequent DB queries. Default value is {@link + * #PROGRAM_PREFETCH_UPDATE_WAIT_MS} */ @VisibleForTesting void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) { mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs; } - /** - * Starts the manager. - */ + /** Starts the manager. */ public void start() { if (mStarted) { return; @@ -172,8 +195,7 @@ public class ProgramDataManager implements MemoryManageable { if (mPrefetchEnabled) { mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); } - mContentResolver.registerContentObserver(Programs.CONTENT_URI, - true, mProgramObserver); + mContentResolver.registerContentObserver(Programs.CONTENT_URI, true, mProgramObserver); } /** @@ -214,9 +236,7 @@ public class ProgramDataManager implements MemoryManageable { return new ArrayList<>(mChannelIdCurrentProgramMap.values()); } - /** - * Reloads program data. - */ + /** Reloads program data. */ public void reload() { if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) { mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS); @@ -226,35 +246,27 @@ public class ProgramDataManager implements MemoryManageable { } } - /** - * A listener interface to receive notification on program data retrieval from DB. - */ + /** A listener interface to receive notification on program data retrieval from DB. */ public interface Listener { /** - * Called when a Program data is now available through getProgram() - * after the DB operation is done which wasn't before. - * This would be called only if fetched data is around the selected program. - **/ + * Called when a Program data is now available through getProgram() after the DB operation + * is done which wasn't before. This would be called only if fetched data is around the + * selected program. + */ void onProgramUpdated(); } - /** - * Adds the {@link Listener}. - */ + /** Adds the {@link Listener}. */ public void addListener(Listener listener) { mListeners.add(listener); } - /** - * Removes the {@link Listener}. - */ + /** Removes the {@link Listener}. */ public void removeListener(Listener listener) { mListeners.remove(listener); } - /** - * Enables or Disables program prefetch. - */ + /** Enables or Disables program prefetch. */ public void setPrefetchEnabled(boolean enable) { if (mPrefetchEnabled == enable) { return; @@ -276,10 +288,10 @@ public class ProgramDataManager implements MemoryManageable { /** * Returns the programs for the given channel which ends after the given start time. * - * <p> Prefetch should be enabled to call it. + * <p>Prefetch should be enabled to call it. * * @return {@link List} with Programs. It may includes dummy program if the entry needs DB - * operations to get. + * operations to get. */ public List<Program> getPrograms(long channelId, long startTime) { SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); @@ -292,9 +304,12 @@ public class ProgramDataManager implements MemoryManageable { cachedPrograms.subList(startIndex, cachedPrograms.size())); } - // Returns the index of program that is played at the specified time. - // If there isn't, return the first program among programs that starts after the given time - // if returnNextProgram is {@code true}. + /** + * Returns the index of program that is played at the specified time. + * + * <p>If there isn't, return the first program among programs that starts after the given time + * if returnNextProgram is {@code true}. + */ private int getProgramIndexAt(List<Program> programs, long time) { Program key = mZeroLengthProgramCache.get(time); if (key == null) { @@ -321,38 +336,38 @@ public class ProgramDataManager implements MemoryManageable { * Adds the listener to be notified if current program is updated for a channel. * * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the - * listener would be called whenever a current program is updated. + * listener would be called whenever a current program is updated. */ public void addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener) { - mChannelId2ProgramUpdatedListeners - .put(channelId, listener); + mChannelId2ProgramUpdatedListeners.put(channelId, listener); } /** - * Removes the listener previously added by - * {@link #addOnCurrentProgramUpdatedListener(long, OnCurrentProgramUpdatedListener)}. + * Removes the listener previously added by {@link #addOnCurrentProgramUpdatedListener(long, + * OnCurrentProgramUpdatedListener)}. */ public void removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener) { - mChannelId2ProgramUpdatedListeners - .remove(channelId, listener); + mChannelId2ProgramUpdatedListeners.remove(channelId, listener); } private void notifyCurrentProgramUpdate(long channelId, Program program) { - for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners - .get(channelId)) { + for (OnCurrentProgramUpdatedListener listener : + mChannelId2ProgramUpdatedListeners.get(channelId)) { listener.onCurrentProgramUpdated(channelId, program); } - for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners - .get(Channel.INVALID_ID)) { + for (OnCurrentProgramUpdatedListener listener : + mChannelId2ProgramUpdatedListeners.get(Channel.INVALID_ID)) { listener.onCurrentProgramUpdated(channelId, program); } } private void updateCurrentProgram(long channelId, Program program) { - Program previousProgram = program == null ? mChannelIdCurrentProgramMap.remove(channelId) - : mChannelIdCurrentProgramMap.put(channelId, program); + Program previousProgram = + program == null + ? mChannelIdCurrentProgramMap.remove(channelId) + : mChannelIdCurrentProgramMap.put(channelId, program); if (!Objects.equals(program, previousProgram)) { if (mPrefetchEnabled) { removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program); @@ -362,20 +377,23 @@ public class ProgramDataManager implements MemoryManageable { long delayedTime; if (program == null) { - delayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS - + (long) (Math.random() * (PERIODIC_PROGRAM_UPDATE_MAX_MS - - PERIODIC_PROGRAM_UPDATE_MIN_MS)); + delayedTime = + PERIODIC_PROGRAM_UPDATE_MIN_MS + + (long) + (Math.random() + * (PERIODIC_PROGRAM_UPDATE_MAX_MS + - PERIODIC_PROGRAM_UPDATE_MIN_MS)); } else { delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis(); } - mHandler.sendMessageDelayed(mHandler.obtainMessage( - MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime); } private void removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram) { SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); - if (!Program.isValid(currentProgram)) { + if (!Program.isProgramValid(currentProgram)) { return; } ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId); @@ -391,27 +409,29 @@ public class ProgramDataManager implements MemoryManageable { continue; } - if (cachedProgram.getEndTimeUtcMillis() <= currentProgram - .getStartTimeUtcMillis()) { + if (cachedProgram.getEndTimeUtcMillis() <= currentProgram.getStartTimeUtcMillis()) { // Keep the programs that ends earlier than current program // but later than mPrefetchTimeRangeStartMs. continue; } // Update dummy program around current program if any. - if (cachedProgram.getStartTimeUtcMillis() < currentProgram - .getStartTimeUtcMillis()) { + if (cachedProgram.getStartTimeUtcMillis() < currentProgram.getStartTimeUtcMillis()) { // The dummy program starts earlier than the current program. Adjust its end time. - i.set(createDummyProgram(cachedProgram.getStartTimeUtcMillis(), - currentProgram.getStartTimeUtcMillis())); + i.set( + createDummyProgram( + cachedProgram.getStartTimeUtcMillis(), + currentProgram.getStartTimeUtcMillis())); i.add(currentProgram); } else { i.set(currentProgram); } if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) { // The dummy program ends later than the current program. Adjust its start time. - i.add(createDummyProgram(currentProgram.getEndTimeUtcMillis(), - cachedProgram.getEndTimeUtcMillis())); + i.add( + createDummyProgram( + currentProgram.getEndTimeUtcMillis(), + cachedProgram.getEndTimeUtcMillis())); } break; } @@ -425,8 +445,8 @@ public class ProgramDataManager implements MemoryManageable { private void handleUpdateCurrentPrograms() { if (mProgramsUpdateTask != null) { - mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CURRENT_PROGRAMS, - CURRENT_PROGRAM_UPDATE_WAIT_MS); + mHandler.sendEmptyMessageDelayed( + MSG_UPDATE_CURRENT_PROGRAMS, CURRENT_PROGRAM_UPDATE_WAIT_MS); return; } clearTask(mProgramUpdateTaskMap); @@ -443,10 +463,13 @@ public class ProgramDataManager implements MemoryManageable { private boolean mSuccess; public ProgramsPrefetchTask() { + super(mDbExecutor); long time = mClock.currentTimeMillis(); - mStartTimeMs = Utils - .floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS); - mEndTimeMs = mStartTimeMs + PROGRAM_GUIDE_MAX_TIME_RANGE; + mStartTimeMs = + Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS); + mEndTimeMs = + mStartTimeMs + + TimeUnit.HOURS.toMillis(PROGRAM_GUIDE_MAX_HOURS.get(mRemoteConfig)); mSuccess = false; } @@ -454,12 +477,19 @@ public class ProgramDataManager implements MemoryManageable { protected Map<Long, ArrayList<Program>> doInBackground(Void... params) { Map<Long, ArrayList<Program>> programMap = new HashMap<>(); if (DEBUG) { - Log.d(TAG, "Starts programs prefetch. " + Utils.toTimeString(mStartTimeMs) + "-" - + Utils.toTimeString(mEndTimeMs)); + Log.d( + TAG, + "Starts programs prefetch. " + + Utils.toTimeString(mStartTimeMs) + + "-" + + Utils.toTimeString(mEndTimeMs)); } - Uri uri = Programs.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs)) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)).build(); + Uri uri = + Programs.CONTENT_URI + .buildUpon() + .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs)) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)) + .build(); final int RETRY_COUNT = 3; Program lastReadProgram = null; for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) { @@ -467,8 +497,8 @@ public class ProgramDataManager implements MemoryManageable { return null; } programMap.clear(); - try (Cursor c = mContentResolver.query(uri, Program.PROJECTION, null, null, - SORT_BY_TIME)) { + try (Cursor c = + mContentResolver.query(uri, Program.PROJECTION, null, null, SORT_BY_TIME)) { if (c == null) { continue; } @@ -527,14 +557,16 @@ public class ProgramDataManager implements MemoryManageable { long currentTime = mClock.currentTimeMillis(); mLastPrefetchTaskRunMs = currentTime; nextMessageDelayedTime = - Utils.floorTime(mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS, - PROGRAM_GUIDE_SNAP_TIME_MS) - currentTime; + Utils.floorTime( + mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS, + PROGRAM_GUIDE_SNAP_TIME_MS) + - currentTime; } else { nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS; } if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { - mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, - nextMessageDelayedTime); + mHandler.sendEmptyMessageDelayed( + MSG_UPDATE_PREFETCH_PROGRAM, nextMessageDelayedTime); } } } @@ -547,10 +579,18 @@ public class ProgramDataManager implements MemoryManageable { private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> { public ProgramsUpdateTask(ContentResolver contentResolver, long time) { - super(contentResolver, Programs.CONTENT_URI.buildUpon() + super( + mDbExecutor, + contentResolver, + Programs.CONTENT_URI + .buildUpon() .appendQueryParameter(PARAM_START_TIME, String.valueOf(time)) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)).build(), - Program.PROJECTION, null, null, SORT_BY_TIME); + .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)) + .build(), + Program.PROJECTION, + null, + null, + SORT_BY_TIME); } @Override @@ -604,10 +644,17 @@ public class ProgramDataManager implements MemoryManageable { private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> { private final long mChannelId; - private UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId, - long time) { - super(contentResolver, TvContract.buildProgramsUriForChannel(channelId, time, time), - Program.PROJECTION, null, null, SORT_BY_TIME); + + private UpdateCurrentProgramForChannelTask( + ContentResolver contentResolver, long channelId, long time) { + super( + mDbExecutor, + contentResolver, + TvContract.buildProgramsUriForChannel(channelId, time, time), + Program.PROJECTION, + null, + null, + SORT_BY_TIME); mChannelId = channelId; } @@ -638,48 +685,55 @@ public class ProgramDataManager implements MemoryManageable { case MSG_UPDATE_CURRENT_PROGRAMS: handleUpdateCurrentPrograms(); break; - case MSG_UPDATE_ONE_CURRENT_PROGRAM: { - long channelId = (Long) msg.obj; - UpdateCurrentProgramForChannelTask oldTask = mProgramUpdateTaskMap - .get(channelId); - if (oldTask != null) { - oldTask.cancel(true); - } - UpdateCurrentProgramForChannelTask - task = new UpdateCurrentProgramForChannelTask( - mContentResolver, channelId, mClock.currentTimeMillis()); - mProgramUpdateTaskMap.put(channelId, task); - task.executeOnDbThread(); - break; - } - case MSG_UPDATE_PREFETCH_PROGRAM: { - if (isProgramUpdatePaused()) { - return; - } - if (mProgramsPrefetchTask != null) { - mHandler.sendEmptyMessageDelayed(msg.what, mProgramPrefetchUpdateWaitMs); - return; + case MSG_UPDATE_ONE_CURRENT_PROGRAM: + { + long channelId = (Long) msg.obj; + UpdateCurrentProgramForChannelTask oldTask = + mProgramUpdateTaskMap.get(channelId); + if (oldTask != null) { + oldTask.cancel(true); + } + UpdateCurrentProgramForChannelTask task = + new UpdateCurrentProgramForChannelTask( + mContentResolver, channelId, mClock.currentTimeMillis()); + mProgramUpdateTaskMap.put(channelId, task); + task.executeOnDbThread(); + break; } - long delayMillis = mLastPrefetchTaskRunMs + mProgramPrefetchUpdateWaitMs - - mClock.currentTimeMillis(); - if (delayMillis > 0) { - mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, delayMillis); - } else { - mProgramsPrefetchTask = new ProgramsPrefetchTask(); - mProgramsPrefetchTask.executeOnDbThread(); + case MSG_UPDATE_PREFETCH_PROGRAM: + { + if (isProgramUpdatePaused()) { + return; + } + if (mProgramsPrefetchTask != null) { + mHandler.sendEmptyMessageDelayed( + msg.what, mProgramPrefetchUpdateWaitMs); + return; + } + long delayMillis = + mLastPrefetchTaskRunMs + + mProgramPrefetchUpdateWaitMs + - mClock.currentTimeMillis(); + if (delayMillis > 0) { + mHandler.sendEmptyMessageDelayed( + MSG_UPDATE_PREFETCH_PROGRAM, delayMillis); + } else { + mProgramsPrefetchTask = new ProgramsPrefetchTask(); + mProgramsPrefetchTask.executeOnDbThread(); + } + break; } - break; - } + default: + // Do nothing } } } /** - * Pause program update. - * Updating program data will result in UI refresh, - * but UI is fragile to handle it so we'd better disable it for a while. + * Pause program update. Updating program data will result in UI refresh, but UI is fragile to + * handle it so we'd better disable it for a while. * - * <p> Prefetch should be enabled to call it. + * <p>Prefetch should be enabled to call it. */ public void setPauseProgramUpdate(boolean pauseProgramUpdate) { SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); @@ -700,11 +754,10 @@ public class ProgramDataManager implements MemoryManageable { } /** - * Sets program data prefetch time range. - * Any program data that ends before the start time will be removed from the cache later. - * Note that there's no limit for end time. + * Sets program data prefetch time range. Any program data that ends before the start time will + * be removed from the cache later. Note that there's no limit for end time. * - * <p> Prefetch should be enabled to call it. + * <p>Prefetch should be enabled to call it. */ public void setPrefetchTimeRange(long startTimeMs) { SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); @@ -736,7 +789,8 @@ public class ProgramDataManager implements MemoryManageable { return new Program.Builder() .setChannelId(Channel.INVALID_ID) .setStartTimeUtcMillis(startTimeMs) - .setEndTimeUtcMillis(endTimeMs).build(); + .setEndTimeUtcMillis(endTimeMs) + .build(); } @Override diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index 709863cf..e4237bf4 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -17,6 +17,7 @@ package com.android.tv.data; import android.media.tv.TvContentRating; +import com.android.tv.data.api.Channel; public interface StreamInfo { int VIDEO_DEFINITION_LEVEL_UNKNOWN = 0; @@ -28,19 +29,26 @@ public interface StreamInfo { int AUDIO_CHANNEL_COUNT_UNKNOWN = 0; Channel getCurrentChannel(); + TvContentRating getBlockedContentRating(); int getVideoWidth(); + int getVideoHeight(); + float getVideoFrameRate(); + float getVideoDisplayAspectRatio(); + int getVideoDefinitionLevel(); + int getAudioChannelCount(); + boolean hasClosedCaption(); + boolean isVideoAvailable(); - /** - * Returns true, if video or audio is available. - */ + /** Returns true, if video or audio is available. */ boolean isVideoOrAudioAvailable(); + int getVideoUnavailableReason(); } diff --git a/src/com/android/tv/data/TvInputNewComparator.java b/src/com/android/tv/data/TvInputNewComparator.java index acc3e38a..effca970 100644 --- a/src/com/android/tv/data/TvInputNewComparator.java +++ b/src/com/android/tv/data/TvInputNewComparator.java @@ -17,15 +17,11 @@ package com.android.tv.data; import android.media.tv.TvInputInfo; - import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; - import java.util.Comparator; -/** - * Compares TV input such that the new input comes first. - */ +/** Compares TV input such that the new input comes first. */ public class TvInputNewComparator implements Comparator<TvInputInfo> { private final SetupUtils mSetupUtils; private final TvInputManagerHelper mInputManager; diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java index 3edd7b1a..7187efd1 100644 --- a/src/com/android/tv/data/WatchedHistoryManager.java +++ b/src/com/android/tv/data/WatchedHistoryManager.java @@ -1,3 +1,18 @@ +/* + * 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.data; import android.content.Context; @@ -12,9 +27,8 @@ import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; - -import com.android.tv.common.SharedPreferencesUtils; - +import com.android.tv.common.util.SharedPreferencesUtils; +import com.android.tv.data.api.Channel; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,13 +39,14 @@ import java.util.concurrent.TimeUnit; /** * A class to manage watched history. * - * <p>When there is no access to watched table of TvProvider, - * this class is used to build up watched history and to compute recent channels. + * <p>When there is no access to watched table of TvProvider, this class is used to build up watched + * history and to compute recent channels. + * * <p>Note that this class is not thread safe. Please use this on one thread. */ public class WatchedHistoryManager { - private final static String TAG = "WatchedHistoryManager"; - private final static boolean DEBUG = false; + private static final String TAG = "WatchedHistoryManager"; + private static final boolean DEBUG = false; private static final int MAX_HISTORY_SIZE = 10000; private static final String PREF_KEY_LAST_INDEX = "last_index"; @@ -47,8 +62,8 @@ public class WatchedHistoryManager { new OnSharedPreferenceChangeListener() { @Override @MainThread - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, String key) { if (key.equals(PREF_KEY_LAST_INDEX)) { final long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1); if (lastIndex <= mLastIndex) { @@ -57,23 +72,26 @@ public class WatchedHistoryManager { // onSharedPreferenceChanged is always called in a main thread. // onNewRecordAdded will be called in the same thread as the thread // which created this instance. - mHandler.post(new Runnable() { - @Override - public void run() { - for (long i = mLastIndex + 1; i <= lastIndex; ++i) { - WatchedRecord record = decode( - mSharedPreferences.getString(getSharedPreferencesKey(i), - null)); - if (record != null) { - mWatchedHistory.add(record); - if (mListener != null) { - mListener.onNewRecordAdded(record); + mHandler.post( + new Runnable() { + @Override + public void run() { + for (long i = mLastIndex + 1; i <= lastIndex; ++i) { + WatchedRecord record = + decode( + mSharedPreferences.getString( + getSharedPreferencesKey(i), + null)); + if (record != null) { + mWatchedHistory.add(record); + if (mListener != null) { + mListener.onNewRecordAdded(record); + } + } } + mLastIndex = lastIndex; } - } - mLastIndex = lastIndex; - } - }); + }); } } }; @@ -94,9 +112,7 @@ public class WatchedHistoryManager { mHandler = new Handler(); } - /** - * Starts the manager. It loads history data from {@link SharedPreferences}. - */ + /** Starts the manager. It loads history data from {@link SharedPreferences}. */ public void start() { if (mStarted) { return; @@ -123,22 +139,22 @@ public class WatchedHistoryManager { @WorkerThread private void loadWatchedHistory() { - mSharedPreferences = mContext.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE); + mSharedPreferences = + mContext.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE); mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1); if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) { for (int i = 0; i <= mLastIndex; ++i) { WatchedRecord record = - decode(mSharedPreferences.getString(getSharedPreferencesKey(i), - null)); + decode(mSharedPreferences.getString(getSharedPreferencesKey(i), null)); if (record != null) { mWatchedHistory.add(record); } } } else if (mLastIndex >= mMaxHistorySize) { for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) { - WatchedRecord record = decode(mSharedPreferences.getString( - getSharedPreferencesKey(i), null)); + WatchedRecord record = + decode(mSharedPreferences.getString(getSharedPreferencesKey(i), null)); if (record != null) { mWatchedHistory.add(record); } @@ -173,9 +189,7 @@ public class WatchedHistoryManager { return mLoaded; } - /** - * Logs the record of the watched channel. - */ + /** Logs the record of the watched channel. */ public void logChannelViewStop(Channel channel, long endTime, long duration) { if (duration < MIN_DURATION_MS) { return; @@ -185,7 +199,8 @@ public class WatchedHistoryManager { if (DEBUG) Log.d(TAG, "Log a watched record. " + record); mWatchedHistory.add(record); ++mLastIndex; - mSharedPreferences.edit() + mSharedPreferences + .edit() .putString(getSharedPreferencesKey(mLastIndex), encode(record)) .putLong(PREF_KEY_LAST_INDEX, mLastIndex) .apply(); @@ -197,16 +212,14 @@ public class WatchedHistoryManager { } } - /** - * Sets {@link Listener}. - */ + /** Sets {@link Listener}. */ public void setListener(Listener listener) { mListener = listener; } /** - * Returns watched history in the ascending order of time. In other words, the first element - * is the oldest and the last element is the latest record. + * Returns watched history in the ascending order of time. In other words, the first element is + * the oldest and the last element is the latest record. */ @NonNull public List<WatchedRecord> getWatchedHistory() { @@ -242,8 +255,12 @@ public class WatchedHistoryManager { @Override public String toString() { - return "WatchedRecord: id=" + channelId + ",watchedStartTime=" + watchedStartTime - + ",duration=" + duration; + return "WatchedRecord: id=" + + channelId + + ",watchedStartTime=" + + watchedStartTime + + ",duration=" + + duration; } @Override @@ -281,10 +298,9 @@ public class WatchedHistoryManager { } public interface Listener { - /** - * Called when history is loaded. - */ + /** Called when history is loaded. */ void onLoadFinished(); + void onNewRecordAdded(WatchedRecord watchedRecord); } } diff --git a/src/com/android/tv/data/api/Channel.java b/src/com/android/tv/data/api/Channel.java new file mode 100644 index 00000000..496331cf --- /dev/null +++ b/src/com/android/tv/data/api/Channel.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tv.data.api; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.android.tv.util.images.ImageLoader.ImageLoaderCallback; + +/** + * Interface for {@link com.android.tv.data.ChannelImpl}. + * + * <p><b>NOTE</b> Normally you should not use an interface for a data object like {@code + * ChannelImpl}, however there are many circular dependencies. An interface is the easiest way to + * break the cycles. + */ +public interface Channel { + + long INVALID_ID = -1; + int LOAD_IMAGE_TYPE_CHANNEL_LOGO = 1; + int LOAD_IMAGE_TYPE_APP_LINK_ICON = 2; + int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; + /** + * When a TIS doesn't provide any information about app link, and it doesn't have a leanback + * launch intent, there will be no app link card for the TIS. + */ + int APP_LINK_TYPE_NONE = -1; + /** + * When a TIS provide a specific app link information, the app link card will be {@code + * APP_LINK_TYPE_CHANNEL} which contains all the provided information. + */ + int APP_LINK_TYPE_CHANNEL = 1; + /** + * When a TIS doesn't provide a specific app link information, but the app has a leanback launch + * intent, the app link card will be {@code APP_LINK_TYPE_APP} which launches the application. + */ + int APP_LINK_TYPE_APP = 2; + /** Channel number delimiter between major and minor parts. */ + char CHANNEL_NUMBER_DELIMITER = '-'; + + long getId(); + + Uri getUri(); + + String getPackageName(); + + String getInputId(); + + String getType(); + + String getDisplayNumber(); + + @Nullable + String getDisplayName(); + + String getDescription(); + + String getVideoFormat(); + + boolean isPassthrough(); + + String getDisplayText(); + + String getAppLinkText(); + + int getAppLinkColor(); + + String getAppLinkIconUri(); + + String getAppLinkPosterArtUri(); + + String getAppLinkIntentUri(); + + String getLogoUri(); + + boolean isRecordingProhibited(); + + boolean isPhysicalTunerChannel(); + + boolean isBrowsable(); + + boolean isSearchable(); + + boolean isLocked(); + + boolean hasSameReadOnlyInfo(Channel mCurrentChannel); + + void setChannelLogoExist(boolean result); + + void setBrowsable(boolean browsable); + + void setLocked(boolean locked); + + void copyFrom(Channel channel); + + void setLogoUri(String logoUri); + + boolean channelLogoExists(); + + void loadBitmap( + Context context, + int loadImageTypeChannelLogo, + int mChannelLogoImageViewWidth, + int mChannelLogoImageViewHeight, + ImageLoaderCallback<?> channelLogoCallback); + + int getAppLinkType(Context context); + + Intent getAppLinkIntent(Context context); + + void prefetchImage( + Context mContext, + int loadImageTypeChannelLogo, + int mPosterArtWidth, + int mPosterArtHeight); +} diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java new file mode 100644 index 00000000..795ad5c4 --- /dev/null +++ b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.data.epg; + +import com.android.tv.data.api.Channel; + +/** + * Hand copy of generated Autovalue class. + * + * TODO get autovalue working + */ +final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel { + + private final Channel channel; + private final String epgChannelId; + + AutoValue_EpgReader_EpgChannel( + Channel channel, + String epgChannelId) { + if (channel == null) { + throw new NullPointerException("Null channel"); + } + this.channel = channel; + if (epgChannelId == null) { + throw new NullPointerException("Null epgChannelId"); + } + this.epgChannelId = epgChannelId; + } + + @Override + public Channel getChannel() { + return channel; + } + + @Override + public String getEpgChannelId() { + return epgChannelId; + } + + @Override + public String toString() { + return "EpgChannel{" + + "channel=" + channel + ", " + + "epgChannelId=" + epgChannelId + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof EpgReader.EpgChannel) { + EpgReader.EpgChannel that = (EpgReader.EpgChannel) o; + return (this.channel.equals(that.getChannel())) + && (this.epgChannelId.equals(that.getEpgChannelId())); + } + return false; + } + + @Override + public int hashCode() { + int h = 1; + h *= 1000003; + h ^= this.channel.hashCode(); + h *= 1000003; + h ^= this.epgChannelId.hashCode(); + return h; + } + +} + diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java index 5693c877..3c7112ec 100644 --- a/src/com/android/tv/data/epg/EpgFetchHelper.java +++ b/src/com/android/tv/data/epg/EpgFetchHelper.java @@ -27,15 +27,15 @@ import android.preference.PreferenceManager; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; - +import com.android.tv.common.CommonConstants; +import com.android.tv.common.util.Clock; import com.android.tv.data.Program; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -/** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */ +/** The helper class for {@link EpgFetcher} */ class EpgFetchHelper { private static final String TAG = "EpgFetchHelper"; private static final boolean DEBUG = false; @@ -45,15 +45,15 @@ class EpgFetchHelper { // Value: Long private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP = - "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp"; + CommonConstants.BASE_PACKAGE + ".data.epg.EpgFetcher.LastUpdatedEpgTimestamp"; // Value: String private static final String KEY_LAST_LINEUP_ID = - "com.android.tv.data.epg.EpgFetcher.LastLineupId"; + CommonConstants.BASE_PACKAGE + ".data.epg.EpgFetcher.LastLineupId"; private static long sLastEpgUpdatedTimestamp = -1; private static String sLastLineupId; - private EpgFetchHelper() { } + private EpgFetchHelper() {} /** * Updates newly fetched EPG data for the given channel to local providers. The method will @@ -61,18 +61,19 @@ class EpgFetchHelper { * of that channel in the database one by one. It will update the matched old program, or insert * the new program if there is no matching program can be found in the database and at the same * time remove those old programs which conflicts with the inserted one. - + * * @param channelId the target channel ID. * @param fetchedPrograms the newly fetched program data. * @return {@code true} if new program data are successfully updated. Otherwise {@code false}. */ - static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) { + static boolean updateEpgData( + Context context, Clock clock, long channelId, List<Program> fetchedPrograms) { final int fetchedProgramsCount = fetchedPrograms.size(); if (fetchedProgramsCount == 0) { return false; } boolean updated = false; - long startTimeMs = System.currentTimeMillis(); + long startTimeMs = clock.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS; List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs); int oldProgramsIndex = 0; @@ -82,8 +83,10 @@ class EpgFetchHelper { // or insert new program if there is no matching program in the database. ArrayList<ContentProviderOperation> ops = new ArrayList<>(); while (newProgramsIndex < fetchedProgramsCount) { - Program oldProgram = oldProgramsIndex < oldPrograms.size() - ? oldPrograms.get(oldProgramsIndex) : null; + Program oldProgram = + oldProgramsIndex < oldPrograms.size() + ? oldPrograms.get(oldProgramsIndex) + : null; Program newProgram = fetchedPrograms.get(newProgramsIndex); boolean addNewProgram = false; if (oldProgram != null) { @@ -95,18 +98,20 @@ class EpgFetchHelper { // Partial match. Update the old program with the new one. // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There // could be application specific settings which belong to the old program. - ops.add(ContentProviderOperation.newUpdate( - TvContract.buildProgramUri(oldProgram.getId())) - .withValues(Program.toContentValues(newProgram)) - .build()); + ops.add( + ContentProviderOperation.newUpdate( + TvContract.buildProgramUri(oldProgram.getId())) + .withValues(Program.toContentValues(newProgram)) + .build()); oldProgramsIndex++; newProgramsIndex++; } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) { // No match. Remove the old program first to see if the next program in // {@code oldPrograms} partially matches the new program. - ops.add(ContentProviderOperation.newDelete( - TvContract.buildProgramUri(oldProgram.getId())) - .build()); + ops.add( + ContentProviderOperation.newDelete( + TvContract.buildProgramUri(oldProgram.getId())) + .build()); oldProgramsIndex++; } else { // No match. The new program does not match any of the old programs. Insert @@ -120,10 +125,10 @@ class EpgFetchHelper { newProgramsIndex++; } if (addNewProgram) { - ops.add(ContentProviderOperation - .newInsert(Programs.CONTENT_URI) - .withValues(Program.toContentValues(newProgram)) - .build()); + ops.add( + ContentProviderOperation.newInsert(Programs.CONTENT_URI) + .withValues(Program.toContentValues(newProgram)) + .build()); } // Throttle the batch operation not to cause TransactionTooLargeException. if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) { @@ -150,11 +155,17 @@ class EpgFetchHelper { return updated; } - private static List<Program> queryPrograms(Context context, long channelId, - long startTimeMs, long endTimeMs) { - try (Cursor c = context.getContentResolver().query( - TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs), - Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) { + private static List<Program> queryPrograms( + Context context, long channelId, long startTimeMs, long endTimeMs) { + try (Cursor c = + context.getContentResolver() + .query( + TvContract.buildProgramsUriForChannel( + channelId, startTimeMs, endTimeMs), + Program.PROJECTION, + null, + null, + Programs.COLUMN_START_TIME_UTC_MILLIS)) { if (c == null) { return Collections.emptyList(); } @@ -167,8 +178,8 @@ class EpgFetchHelper { } /** - * Returns {@code true} if the {@code oldProgram} needs to be updated with the - * {@code newProgram}. + * Returns {@code true} if the {@code oldProgram} needs to be updated with the {@code + * newProgram}. */ private static boolean hasSameTitleAndOverlap(Program oldProgram, Program newProgram) { // NOTE: Here, we update the old program if it has the same title and overlaps with the @@ -186,24 +197,25 @@ class EpgFetchHelper { * every time when it needs to fetch EPG data. */ @WorkerThread - synchronized static void setLastLineupId(Context context, String lineupId) { + static synchronized void setLastLineupId(Context context, String lineupId) { if (DEBUG) { if (lineupId == null) { Log.d(TAG, "Clear stored lineup id: " + sLastLineupId); } } sLastLineupId = lineupId; - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putString(KEY_LAST_LINEUP_ID, lineupId).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(KEY_LAST_LINEUP_ID, lineupId) + .apply(); } - /** - * Gets the last known lineup ID from shared preferences. - */ - synchronized static String getLastLineupId(Context context) { + /** Gets the last known lineup ID from shared preferences. */ + static synchronized String getLastLineupId(Context context) { if (sLastLineupId == null) { - sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context) - .getString(KEY_LAST_LINEUP_ID, null); + sLastLineupId = + PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LAST_LINEUP_ID, null); } if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId); return sLastLineupId; @@ -214,20 +226,21 @@ class EpgFetchHelper { * out-dated, it's not necessary for EPG fetcher to fetch EPG again. */ @WorkerThread - synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) { + static synchronized void setLastEpgUpdatedTimestamp(Context context, long timestamp) { sLastEpgUpdatedTimestamp = timestamp; - PreferenceManager.getDefaultSharedPreferences(context).edit().putLong( - KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp) + .apply(); } - /** - * Gets the last updated timestamp of EPG data. - */ - synchronized static long getLastEpgUpdatedTimestamp(Context context) { + /** Gets the last updated timestamp of EPG data. */ + static synchronized long getLastEpgUpdatedTimestamp(Context context) { if (sLastEpgUpdatedTimestamp < 0) { - sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context) - .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0); + sLastEpgUpdatedTimestamp = + PreferenceManager.getDefaultSharedPreferences(context) + .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0); } return sLastEpgUpdatedTimestamp; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/epg/EpgFetchService.java b/src/com/android/tv/data/epg/EpgFetchService.java new file mode 100644 index 00000000..aa4f3588 --- /dev/null +++ b/src/com/android/tv/data/epg/EpgFetchService.java @@ -0,0 +1,70 @@ +/* + * 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.data.epg; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; +import com.android.tv.data.ChannelDataManager; + +/** JobService to Fetch EPG data. */ +public class EpgFetchService extends JobService { + private EpgFetcher mEpgFetcher; + private ChannelDataManager mChannelDataManager; + + @Override + public void onCreate() { + super.onCreate(); + Starter.start(this); + TvSingletons tvSingletons = TvSingletons.getSingletons(getApplicationContext()); + mEpgFetcher = tvSingletons.getEpgFetcher(); + mChannelDataManager = tvSingletons.getChannelDataManager(); + } + + @Override + public boolean onStartJob(JobParameters params) { + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener( + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + if (!mEpgFetcher.executeFetchTaskIfPossible( + EpgFetchService.this, params)) { + jobFinished(params, false); + } + } + + @Override + public void onChannelListUpdated() {} + + @Override + public void onChannelBrowsableChanged() {} + }); + return true; + } else { + return mEpgFetcher.executeFetchTaskIfPossible(this, params); + } + } + + @Override + public boolean onStopJob(JobParameters params) { + mEpgFetcher.stopFetchingJob(); + return false; + } +} diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index 24f8b826..9c24613d 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,720 +16,44 @@ package com.android.tv.data.epg; -import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; -import android.content.ComponentName; -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.net.TrafficStats; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.AnyThread; import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; -import android.util.Log; -import com.android.tv.ApplicationSingletons; -import com.android.tv.Features; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; -import com.android.tv.config.RemoteConfigUtils; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.ChannelLogoFetcher; -import com.android.tv.data.Lineup; -import com.android.tv.data.Program; -import com.android.tv.perf.EventNames; -import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.TimerEvent; -import com.android.tv.tuner.util.PostalCodeUtils; -import com.android.tv.util.LocationUtils; -import com.android.tv.util.NetworkTrafficTags; -import com.android.tv.util.Utils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * The service class to fetch EPG routinely or on-demand during channel scanning - * - * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one - * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on - * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}. - */ -public class EpgFetcher { - private static final String TAG = "EpgFetcher"; - private static final boolean DEBUG = false; - - private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101; - - private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10); - - private static final int REASON_EPG_READER_NOT_READY = 1; - private static final int REASON_LOCATION_INFO_UNAVAILABLE = 2; - private static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3; - private static final int REASON_NO_EPG_DATA_RETURNED = 4; - private static final int REASON_NO_NEW_EPG = 5; - - private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10); - - private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); - private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2); - - private static final int DEFAULT_ROUTINE_INTERVAL_HOUR = 4; - private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour"; - - private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1; - private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2; - private static final int MSG_FINISH_FETCH_DURING_SCAN = 3; - private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4; - - private static final int QUERY_CHANNEL_COUNT = 50; - private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3; - - private static EpgFetcher sInstance; - - private final Context mContext; - private final ChannelDataManager mChannelDataManager; - private final EpgReader mEpgReader; - private final PerformanceMonitor mPerformanceMonitor; - private FetchAsyncTask mFetchTask; - private FetchDuringScanHandler mFetchDuringScanHandler; - private long mEpgTimeStamp; - private List<Lineup> mPossibleLineups; - private final Object mPossibleLineupsLock = new Object(); - private final Object mFetchDuringScanHandlerLock = new Object(); - // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished. - private boolean mScanStarted; - - private final long mRoutineIntervalMs; - private final long mEpgDataExpiredTimeLimitMs; - private final long mFastFetchDurationSec; - - public static EpgFetcher getInstance(Context context) { - if (sInstance == null) { - sInstance = new EpgFetcher(context); - } - return sInstance; - } - - /** Creates and returns {@link EpgReader}. */ - public static EpgReader createEpgReader(Context context, String region) { - return new StubEpgReader(context); - } - - private EpgFetcher(Context context) { - mContext = context.getApplicationContext(); - ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext); - mChannelDataManager = applicationSingletons.getChannelDataManager(); - mPerformanceMonitor = applicationSingletons.getPerformanceMonitor(); - mEpgReader = createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext)); - - int remoteInteval = - (int) RemoteConfigUtils.getRemoteConfig( - context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR); - mRoutineIntervalMs = - remoteInteval < 0 - ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR) - : TimeUnit.HOURS.toMillis(remoteInteval); - mEpgDataExpiredTimeLimitMs = mRoutineIntervalMs * 2; - mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + mRoutineIntervalMs / 1000; - } +/** Fetch EPG routinely or on-demand during channel scanning */ +public interface EpgFetcher { /** * Starts the routine service of EPG fetching. It use {@link JobScheduler} to schedule the EPG - * fetching routine. The EPG fetching routine will be started roughly every 4 hours, unless - * the channel scanning of tuner input is started. - */ - @MainThread - public void startRoutineService() { - JobScheduler jobScheduler = - (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); - for (JobInfo job : jobScheduler.getAllPendingJobs()) { - if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) { - return; - } - } - JobInfo job = - new JobInfo.Builder( - EPG_ROUTINELY_FETCHING_JOB_ID, - new ComponentName(mContext, EpgFetchService.class)) - .setPeriodic(mRoutineIntervalMs) - .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL) - .setPersisted(true) - .build(); - jobScheduler.schedule(job); - Log.i(TAG, "EPG fetching routine service started."); - } - - /** - * Fetches EPG immediately if current EPG data are out-dated, i.e., not successfully updated - * by routine fetching service due to various reasons. + * fetching routine. The EPG fetching routine will be started roughly every 4 hours, unless the + * channel scanning of tuner input is started. */ @MainThread - public void fetchImmediatelyIfNeeded() { - if (TvCommonUtils.isRunningInTest()) { - // Do not run EpgFetcher in test. - return; - } - new AsyncTask<Void, Void, Long>() { - @Override - protected Long doInBackground(Void... args) { - return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext); - } - - @Override - protected void onPostExecute(Long result) { - if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { - Log.i(TAG, "EPG data expired. Start fetching immediately."); - fetchImmediately(); - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - /** - * Fetches EPG immediately. - */ - @MainThread - public void fetchImmediately() { - if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener(new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - executeFetchTaskIfPossible(null, null); - } - - @Override - public void onChannelListUpdated() { } - - @Override - public void onChannelBrowsableChanged() { } - }); - } else { - executeFetchTaskIfPossible(null, null); - } - } + void startRoutineService(); /** - * Notifies EPG fetch service that channel scanning is started. + * Fetches EPG immediately if current EPG data are out-dated, i.e., not successfully updated by + * routine fetching service due to various reasons. */ @MainThread - public void onChannelScanStarted() { - if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { - return; - } - mScanStarted = true; - stopFetchingJob(); - synchronized (mFetchDuringScanHandlerLock) { - if (mFetchDuringScanHandler == null) { - HandlerThread thread = new HandlerThread("EpgFetchDuringScan"); - thread.start(); - mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper()); - } - mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN); - } - Log.i(TAG, "EPG fetching on channel scanning started."); - } + void fetchImmediatelyIfNeeded(); - /** - * Notifies EPG fetch service that channel scanning is finished. - */ + /** Fetches EPG immediately. */ @MainThread - public void onChannelScanFinished() { - if (!mScanStarted) { - return; - } - mScanStarted = false; - mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); - } + void fetchImmediately(); + /** Notifies EPG fetch service that channel scanning is started. */ @MainThread - private void stopFetchingJob() { - if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job..."); - if (mFetchTask != null) { - mFetchTask.cancel(true); - mFetchTask = null; - Log.i(TAG, "EPG routinely fetching job stopped."); - } - } + void onChannelScanStarted(); + /** Notifies EPG fetch service that channel scanning is finished. */ @MainThread - private boolean executeFetchTaskIfPossible(JobService service, JobParameters params) { - SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished()); - if (!TvCommonUtils.isRunningInTest() && checkFetchPrerequisite()) { - mFetchTask = new FetchAsyncTask(service, params); - mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - return true; - } - return false; - } + void onChannelScanFinished(); @MainThread - private boolean checkFetchPrerequisite() { - if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job."); - if (!Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { - Log.i(TAG, "Cannot start routine service: country not supported: " - + LocationUtils.getCurrentCountry(mContext)); - return false; - } - if (mFetchTask != null) { - // Fetching job is already running or ready to run, no need to start again. - return false; - } - if (mFetchDuringScanHandler != null) { - if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels."); - return false; - } - if (getTunerChannelCount() == 0) { - if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels."); - return false; - } - if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) { - return true; - } - if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return true; - } - return true; - } + boolean executeFetchTaskIfPossible(JobService jobService, JobParameters params); @MainThread - private int getTunerChannelCount() { - for (TvInputInfo input : TvApplication.getSingletons(mContext) - .getTvInputManagerHelper().getTvInputInfos(true, true)) { - String inputId = input.getId(); - if (Utils.isInternalTvInput(mContext, inputId)) { - return mChannelDataManager.getChannelCountForInput(inputId); - } - } - return 0; - } - - @AnyThread - private void clearUnusedLineups(@Nullable String lineupId) { - synchronized (mPossibleLineupsLock) { - if (mPossibleLineups == null) { - return; - } - for (Lineup lineup : mPossibleLineups) { - if (!TextUtils.equals(lineupId, lineup.id)) { - mEpgReader.clearCachedChannels(lineup.id); - } - } - mPossibleLineups = null; - } - } - - @WorkerThread - private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) { - if (!mEpgReader.isAvailable()) { - Log.i(TAG, "EPG reader is temporarily unavailable."); - return REASON_EPG_READER_NOT_READY; - } - // Checks the EPG Timestamp. - mEpgTimeStamp = mEpgReader.getEpgTimestamp(); - if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) { - if (DEBUG) Log.d(TAG, "No new EPG."); - return REASON_NO_NEW_EPG; - } - // Updates postal code. - boolean postalCodeChanged = false; - try { - postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext); - } catch (IOException e) { - if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return REASON_LOCATION_INFO_UNAVAILABLE; - } - } catch (SecurityException e) { - Log.w(TAG, "No permission to get the current location."); - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return REASON_LOCATION_PERMISSION_NOT_GRANTED; - } - } catch (PostalCodeUtils.NoPostalCodeException e) { - Log.i(TAG, "Cannot get address or postal code."); - return REASON_LOCATION_INFO_UNAVAILABLE; - } - // Updates possible lineups if necessary. - SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset."); - if (postalCodeChanged || forceUpdatePossibleLineups - || EpgFetchHelper.getLastLineupId(mContext) == null) { - // To prevent main thread being blocked, though theoretically it should not happen. - List<Lineup> possibleLineups = - mEpgReader.getLineups(PostalCodeUtils.getLastPostalCode(mContext)); - if (possibleLineups.isEmpty()) { - return REASON_NO_EPG_DATA_RETURNED; - } - for (Lineup lineup : possibleLineups) { - mEpgReader.preloadChannels(lineup.id); - } - synchronized (mPossibleLineupsLock) { - mPossibleLineups = possibleLineups; - } - EpgFetchHelper.setLastLineupId(mContext, null); - } - return null; - } - - @WorkerThread - private void batchFetchEpg(List<Channel> channels, long durationSec) { - Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size()); - if (channels.size() == 0) { - return; - } - List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT); - for (Channel channel : channels) { - queryChannelIds.add(channel.getId()); - if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) { - batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec)); - queryChannelIds.clear(); - } - } - if (!queryChannelIds.isEmpty()) { - batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec)); - } - } - - @WorkerThread - private void batchUpdateEpg(Map<Long, List<Program>> allPrograms) { - for (Map.Entry<Long, List<Program>> entry : allPrograms.entrySet()) { - List<Program> programs = entry.getValue(); - if (programs == null) { - continue; - } - Collections.sort(programs); - Log.i(TAG, "Batch fetched " + programs.size() + " programs for channel " - + entry.getKey()); - EpgFetchHelper.updateEpgData(mContext, entry.getKey(), programs); - } - } - - @Nullable - @WorkerThread - private String pickBestLineupId(List<Channel> currentChannelList) { - String maxLineupId = null; - synchronized (mPossibleLineupsLock) { - if (mPossibleLineups == null) { - return null; - } - int maxCount = 0; - for (Lineup lineup : mPossibleLineups) { - int count = getMatchedChannelCount(lineup.id, currentChannelList); - Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches"); - if (count > maxCount) { - maxCount = count; - maxLineupId = lineup.id; - } - } - } - return maxLineupId; - } - - @WorkerThread - private int getMatchedChannelCount(String lineupId, List<Channel> currentChannelList) { - // Construct a list of display numbers for existing channels. - if (currentChannelList.isEmpty()) { - if (DEBUG) Log.d(TAG, "No existing channel to compare"); - return 0; - } - List<String> numbers = new ArrayList<>(currentChannelList.size()); - for (Channel channel : currentChannelList) { - // We only support channels from internal tuner inputs. - if (Utils.isInternalTvInput(mContext, channel.getInputId())) { - numbers.add(channel.getDisplayNumber()); - } - } - numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); - return numbers.size(); - } - - public static class EpgFetchService extends JobService { - private EpgFetcher mEpgFetcher; - - @Override - public void onCreate() { - super.onCreate(); - TvApplication.setCurrentRunningProcess(this, true); - mEpgFetcher = EpgFetcher.getInstance(this); - } - - @Override - public boolean onStartJob(JobParameters params) { - if (!mEpgFetcher.mChannelDataManager.isDbLoadFinished()) { - mEpgFetcher.mChannelDataManager.addListener(new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - mEpgFetcher.mChannelDataManager.removeListener(this); - if (!mEpgFetcher.executeFetchTaskIfPossible(EpgFetchService.this, params)) { - jobFinished(params, false); - } - } - - @Override - public void onChannelListUpdated() { } - - @Override - public void onChannelBrowsableChanged() { } - }); - return true; - } else { - return mEpgFetcher.executeFetchTaskIfPossible(this, params); - } - } - - @Override - public boolean onStopJob(JobParameters params) { - mEpgFetcher.stopFetchingJob(); - return false; - } - } - - private class FetchAsyncTask extends AsyncTask<Void, Void, Integer> { - private final JobService mService; - private final JobParameters mParams; - private List<Channel> mCurrentChannelList; - private TimerEvent mTimerEvent; - - private FetchAsyncTask(JobService service, JobParameters params) { - mService = service; - mParams = params; - } - - @Override - protected void onPreExecute() { - mTimerEvent = mPerformanceMonitor.startTimer(); - mCurrentChannelList = mChannelDataManager.getChannelList(); - } - - @Override - protected Integer doInBackground(Void... args) { - final int oldTag = TrafficStats.getThreadStatsTag(); - TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH); - try { - if (DEBUG) Log.d(TAG, "Start EPG routinely fetching."); - Integer failureReason = prepareFetchEpg(false); - // InterruptedException might be caught by RPC, we should check it here. - if (failureReason != null || this.isCancelled()) { - return failureReason; - } - String lineupId = EpgFetchHelper.getLastLineupId(mContext); - lineupId = lineupId == null ? pickBestLineupId(mCurrentChannelList) : lineupId; - if (lineupId != null) { - Log.i(TAG, "Selecting the lineup " + lineupId); - // During normal fetching process, the lineup ID should be confirmed since all - // channels are known, clear up possible lineups to save resources. - EpgFetchHelper.setLastLineupId(mContext, lineupId); - clearUnusedLineups(lineupId); - } else { - Log.i(TAG, "Failed to get lineup id"); - return REASON_NO_EPG_DATA_RETURNED; - } - final List<Channel> channels = mEpgReader.getChannels(lineupId); - // InterruptedException might be caught by RPC, we should check it here. - if (this.isCancelled()) { - return null; - } - if (channels.isEmpty()) { - Log.i(TAG, "Failed to get EPG channels."); - return REASON_NO_EPG_DATA_RETURNED; - } - if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { - batchFetchEpg(channels, mFastFetchDurationSec); - } - new Handler(mContext.getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - ChannelLogoFetcher.startFetchingChannelLogos( - mContext, channels); - } - }); - for (Channel channel : channels) { - if (this.isCancelled()) { - return null; - } - long channelId = channel.getId(); - List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channelId)); - // InterruptedException might be caught by RPC, we should check it here. - Collections.sort(programs); - Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channelId); - EpgFetchHelper.updateEpgData(mContext, channelId, programs); - } - EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp); - if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); - return null; - } finally { - TrafficStats.setThreadStatsTag(oldTag); - } - } - - @Override - protected void onPostExecute(Integer failureReason) { - mFetchTask = null; - if (failureReason == null || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED - || failureReason == REASON_NO_NEW_EPG) { - jobFinished(false); - } else { - // Applies back-off policy - jobFinished(true); - } - mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK); - mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK); - } - - @Override - protected void onCancelled(Integer failureReason) { - clearUnusedLineups(null); - jobFinished(false); - } - - private void jobFinished(boolean reschedule) { - if (mService != null && mParams != null) { - // Task is executed from JobService, need to report jobFinished. - mService.jobFinished(mParams, reschedule); - } - } - } - - @WorkerThread - private class FetchDuringScanHandler extends Handler { - private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>(); - private String mPossibleLineupId; - - private final ChannelDataManager.Listener mDuringScanChannelListener = - new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); - if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP - && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - Message.obtain(FetchDuringScanHandler.this, - MSG_CHANNEL_UPDATED_DURING_SCAN, new ArrayList<>( - mChannelDataManager.getChannelList())).sendToTarget(); - } - } - - @Override - public void onChannelListUpdated() { - if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); - if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP - && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - Message.obtain(FetchDuringScanHandler.this, - MSG_CHANNEL_UPDATED_DURING_SCAN, - mChannelDataManager.getChannelList()).sendToTarget(); - } - } - - @Override - public void onChannelBrowsableChanged() { - // Do nothing - } - }; - - @AnyThread - private FetchDuringScanHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_PREPARE_FETCH_DURING_SCAN: - case MSG_RETRY_PREPARE_FETCH_DURING_SCAN: - onPrepareFetchDuringScan(); - break; - case MSG_CHANNEL_UPDATED_DURING_SCAN: - if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - onChannelUpdatedDuringScan((List<Channel>) msg.obj); - } - break; - case MSG_FINISH_FETCH_DURING_SCAN: - removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN); - if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); - } else { - onFinishFetchDuringScan(); - } - break; - } - } - - private void onPrepareFetchDuringScan() { - Integer failureReason = prepareFetchEpg(true); - if (failureReason != null) { - sendEmptyMessageDelayed( - MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS); - return; - } - mChannelDataManager.addListener(mDuringScanChannelListener); - } - - private void onChannelUpdatedDuringScan(List<Channel> currentChannelList) { - String lineupId = pickBestLineupId(currentChannelList); - Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId); - if (TextUtils.isEmpty(lineupId)) { - if (TextUtils.isEmpty(mPossibleLineupId)) { - return; - } - } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) { - mFetchedChannelIdsDuringScan.clear(); - mPossibleLineupId = lineupId; - } - List<Long> currentChannelIds = new ArrayList<>(); - for (Channel channel : currentChannelList) { - currentChannelIds.add(channel.getId()); - } - mFetchedChannelIdsDuringScan.retainAll(currentChannelIds); - List<Channel> newChannels = new ArrayList<>(); - for (Channel channel : mEpgReader.getChannels(mPossibleLineupId)) { - if (!mFetchedChannelIdsDuringScan.contains(channel.getId())) { - newChannels.add(channel); - mFetchedChannelIdsDuringScan.add(channel.getId()); - } - } - batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC); - } - - private void onFinishFetchDuringScan() { - mChannelDataManager.removeListener(mDuringScanChannelListener); - EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId); - clearUnusedLineups(null); - mFetchedChannelIdsDuringScan.clear(); - synchronized (mFetchDuringScanHandlerLock) { - if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) { - removeCallbacksAndMessages(null); - getLooper().quit(); - mFetchDuringScanHandler = null; - } - } - // Clear timestamp to make routine service start right away. - EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0); - Log.i(TAG, "EPG Fetching during channel scanning finished."); - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - fetchImmediately(); - } - }); - } - } + void stopFetchingJob(); } diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java new file mode 100644 index 00000000..2aaaa5b2 --- /dev/null +++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java @@ -0,0 +1,811 @@ +/* + * 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.data.epg; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; +import android.net.TrafficStats; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +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.TextUtils; +import android.util.Log; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; +import com.android.tv.common.BuildConfig; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.config.api.RemoteConfigValue; +import com.android.tv.common.util.Clock; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.LocationUtils; +import com.android.tv.common.util.NetworkTrafficTags; +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.common.util.PostalCodeUtils; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelImpl; +import com.android.tv.data.ChannelLogoFetcher; +import com.android.tv.data.Lineup; +import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.android.tv.perf.EventNames; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.TimerEvent; +import com.android.tv.util.Utils; +import com.google.android.tv.partner.support.EpgInput; +import com.google.android.tv.partner.support.EpgInputs; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * The service class to fetch EPG routinely or on-demand during channel scanning + * + * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one + * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on + * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}. + */ +public class EpgFetcherImpl implements EpgFetcher { + private static final String TAG = "EpgFetcherImpl"; + private static final boolean DEBUG = false; + + private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101; + + private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10); + + @VisibleForTesting static final int REASON_EPG_READER_NOT_READY = 1; + @VisibleForTesting static final int REASON_LOCATION_INFO_UNAVAILABLE = 2; + @VisibleForTesting static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3; + @VisibleForTesting static final int REASON_NO_EPG_DATA_RETURNED = 4; + @VisibleForTesting static final int REASON_NO_NEW_EPG = 5; + @VisibleForTesting static final int REASON_ERROR = 6; + @VisibleForTesting static final int REASON_CLOUD_EPG_FAILURE = 7; + @VisibleForTesting static final int REASON_NO_BUILT_IN_CHANNELS = 8; + + private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10); + + private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); + private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2); + + private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR = + RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4); + + private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1; + private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2; + private static final int MSG_FINISH_FETCH_DURING_SCAN = 3; + private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4; + + private static final int QUERY_CHANNEL_COUNT = 50; + private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3; + + private final Context mContext; + private final ChannelDataManager mChannelDataManager; + private final EpgReader mEpgReader; + private final PerformanceMonitor mPerformanceMonitor; + private FetchAsyncTask mFetchTask; + private FetchDuringScanHandler mFetchDuringScanHandler; + private long mEpgTimeStamp; + private List<Lineup> mPossibleLineups; + private final Object mPossibleLineupsLock = new Object(); + private final Object mFetchDuringScanHandlerLock = new Object(); + // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished. + private boolean mScanStarted; + + private final long mRoutineIntervalMs; + private final long mEpgDataExpiredTimeLimitMs; + private final long mFastFetchDurationSec; + private Clock mClock; + + public static EpgFetcher create(Context context) { + context = context.getApplicationContext(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager(); + PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor(); + EpgReader epgReader = tvSingletons.providesEpgReader().get(); + Clock clock = tvSingletons.getClock(); + long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig()); + + return new EpgFetcherImpl( + context, + channelDataManager, + epgReader, + performanceMonitor, + clock, + routineIntervalMs); + } + + @VisibleForTesting + EpgFetcherImpl( + Context context, + ChannelDataManager channelDataManager, + EpgReader epgReader, + PerformanceMonitor performanceMonitor, + Clock clock, + long routineIntervalMs) { + mContext = context; + mChannelDataManager = channelDataManager; + mEpgReader = epgReader; + mPerformanceMonitor = performanceMonitor; + mClock = clock; + mRoutineIntervalMs = + routineIntervalMs <= 0 + ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue()) + : TimeUnit.HOURS.toMillis(routineIntervalMs); + mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2; + mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000; + } + + private static Set<Channel> getExistingChannelsForMyPackage(Context context) { + HashSet<Channel> channels = new HashSet<>(); + String selection = null; + String[] selectionArgs = null; + String myPackageName = context.getPackageName(); + if (PermissionUtils.hasAccessAllEpg(context)) { + selection = "package_name=?"; + selectionArgs = new String[] {myPackageName}; + } + try (Cursor c = + context.getContentResolver() + .query( + TvContract.Channels.CONTENT_URI, + ChannelImpl.PROJECTION, + selection, + selectionArgs, + null)) { + if (c != null) { + while (c.moveToNext()) { + Channel channel = ChannelImpl.fromCursor(c); + if (DEBUG) Log.d(TAG, "Found " + channel); + if (myPackageName.equals(channel.getPackageName())) { + channels.add(channel); + } + } + } + } + if (DEBUG) + Log.d(TAG, "Found " + channels.size() + " channels for package " + myPackageName); + return channels; + } + + @Override + @MainThread + public void startRoutineService() { + JobScheduler jobScheduler = + (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); + for (JobInfo job : jobScheduler.getAllPendingJobs()) { + if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) { + return; + } + } + JobInfo job = + new JobInfo.Builder( + EPG_ROUTINELY_FETCHING_JOB_ID, + new ComponentName(mContext, EpgFetchService.class)) + .setPeriodic(mRoutineIntervalMs) + .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL) + .setPersisted(true) + .build(); + jobScheduler.schedule(job); + Log.i(TAG, "EPG fetching routine service started."); + } + + @Override + @MainThread + public void fetchImmediatelyIfNeeded() { + if (CommonUtils.isRunningInTest()) { + // Do not run EpgFetcher in test. + return; + } + new AsyncTask<Void, Void, Long>() { + @Override + protected Long doInBackground(Void... args) { + return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext); + } + + @Override + protected void onPostExecute(Long result) { + if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) + > mEpgDataExpiredTimeLimitMs) { + Log.i(TAG, "EPG data expired. Start fetching immediately."); + fetchImmediately(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + @MainThread + public void fetchImmediately() { + if (DEBUG) Log.d(TAG, "fetchImmediately"); + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener( + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + executeFetchTaskIfPossible(null, null); + } + + @Override + public void onChannelListUpdated() {} + + @Override + public void onChannelBrowsableChanged() {} + }); + } else { + executeFetchTaskIfPossible(null, null); + } + } + + @Override + @MainThread + public void onChannelScanStarted() { + if (mScanStarted || !TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { + return; + } + mScanStarted = true; + stopFetchingJob(); + synchronized (mFetchDuringScanHandlerLock) { + if (mFetchDuringScanHandler == null) { + HandlerThread thread = new HandlerThread("EpgFetchDuringScan"); + thread.start(); + mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper()); + } + mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN); + } + Log.i(TAG, "EPG fetching on channel scanning started."); + } + + @Override + @MainThread + public void onChannelScanFinished() { + if (!mScanStarted) { + return; + } + mScanStarted = false; + mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); + } + + @MainThread + @Override + public void stopFetchingJob() { + if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job..."); + if (mFetchTask != null) { + mFetchTask.cancel(true); + mFetchTask = null; + Log.i(TAG, "EPG routinely fetching job stopped."); + } + } + + @MainThread + @Override + public boolean executeFetchTaskIfPossible(JobService service, JobParameters params) { + if (DEBUG) Log.d(TAG, "executeFetchTaskIfPossible"); + SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished()); + if (!CommonUtils.isRunningInTest() && checkFetchPrerequisite()) { + mFetchTask = createFetchTask(service, params); + mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return true; + } + return false; + } + + @VisibleForTesting + FetchAsyncTask createFetchTask(JobService service, JobParameters params) { + return new FetchAsyncTask(service, params); + } + + @MainThread + private boolean checkFetchPrerequisite() { + if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job."); + if (!TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { + Log.i( + TAG, + "Cannot start routine service: country not supported: " + + LocationUtils.getCurrentCountry(mContext)); + return false; + } + if (mFetchTask != null) { + // Fetching job is already running or ready to run, no need to start again. + return false; + } + if (mFetchDuringScanHandler != null) { + if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels."); + return false; + } + return true; + } + + @MainThread + private int getTunerChannelCount() { + for (TvInputInfo input : + TvSingletons.getSingletons(mContext) + .getTvInputManagerHelper() + .getTvInputInfos(true, true)) { + String inputId = input.getId(); + if (Utils.isInternalTvInput(mContext, inputId)) { + return mChannelDataManager.getChannelCountForInput(inputId); + } + } + return 0; + } + + @AnyThread + private void clearUnusedLineups(@Nullable String lineupId) { + synchronized (mPossibleLineupsLock) { + if (mPossibleLineups == null) { + return; + } + for (Lineup lineup : mPossibleLineups) { + if (!TextUtils.equals(lineupId, lineup.getId())) { + mEpgReader.clearCachedChannels(lineup.getId()); + } + } + mPossibleLineups = null; + } + } + + @WorkerThread + private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) { + if (!mEpgReader.isAvailable()) { + Log.i(TAG, "EPG reader is temporarily unavailable."); + return REASON_EPG_READER_NOT_READY; + } + // Checks the EPG Timestamp. + mEpgTimeStamp = mEpgReader.getEpgTimestamp(); + if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) { + if (DEBUG) Log.d(TAG, "No new EPG."); + return REASON_NO_NEW_EPG; + } + // Updates postal code. + boolean postalCodeChanged = false; + try { + postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext); + } catch (IOException e) { + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return REASON_LOCATION_INFO_UNAVAILABLE; + } + } catch (SecurityException e) { + Log.w(TAG, "No permission to get the current location."); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return REASON_LOCATION_PERMISSION_NOT_GRANTED; + } + } catch (PostalCodeUtils.NoPostalCodeException e) { + Log.i(TAG, "Cannot get address or postal code."); + return REASON_LOCATION_INFO_UNAVAILABLE; + } + // Updates possible lineups if necessary. + SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset."); + if (postalCodeChanged + || forceUpdatePossibleLineups + || EpgFetchHelper.getLastLineupId(mContext) == null) { + // To prevent main thread being blocked, though theoretically it should not happen. + String lastPostalCode = PostalCodeUtils.getLastPostalCode(mContext); + List<Lineup> possibleLineups = mEpgReader.getLineups(lastPostalCode); + if (possibleLineups.isEmpty()) { + Log.i(TAG, "No lineups found for " + lastPostalCode); + return REASON_NO_EPG_DATA_RETURNED; + } + for (Lineup lineup : possibleLineups) { + mEpgReader.preloadChannels(lineup.getId()); + } + synchronized (mPossibleLineupsLock) { + mPossibleLineups = possibleLineups; + } + EpgFetchHelper.setLastLineupId(mContext, null); + } + return null; + } + + @WorkerThread + private void batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec) { + Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size()); + if (epgChannels.size() == 0) { + return; + } + Set<EpgReader.EpgChannel> batch = new HashSet<>(QUERY_CHANNEL_COUNT); + for (EpgReader.EpgChannel epgChannel : epgChannels) { + batch.add(epgChannel); + if (batch.size() >= QUERY_CHANNEL_COUNT) { + batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec)); + batch.clear(); + } + } + if (!batch.isEmpty()) { + batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec)); + } + } + + @WorkerThread + private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) { + for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) { + List<Program> programs = new ArrayList(entry.getValue()); + if (programs == null) { + continue; + } + Collections.sort(programs); + Log.i( + TAG, + "Batch fetched " + programs.size() + " programs for channel " + entry.getKey()); + EpgFetchHelper.updateEpgData( + mContext, mClock, entry.getKey().getChannel().getId(), programs); + } + } + + @Nullable + @WorkerThread + private String pickBestLineupId(Set<Channel> currentChannels) { + String maxLineupId = null; + synchronized (mPossibleLineupsLock) { + if (mPossibleLineups == null) { + return null; + } + int maxCount = 0; + for (Lineup lineup : mPossibleLineups) { + int count = getMatchedChannelCount(lineup.getId(), currentChannels); + Log.i(TAG, lineup.getName() + " (" + lineup.getId() + ") - " + count + " matches"); + if (count > maxCount) { + maxCount = count; + maxLineupId = lineup.getId(); + } + } + } + return maxLineupId; + } + + @WorkerThread + private int getMatchedChannelCount(String lineupId, Set<Channel> currentChannels) { + // Construct a list of display numbers for existing channels. + if (currentChannels.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing channel to compare"); + return 0; + } + List<String> numbers = new ArrayList<>(currentChannels.size()); + for (Channel channel : currentChannels) { + // We only support channels from internal tuner inputs. + if (Utils.isInternalTvInput(mContext, channel.getInputId())) { + numbers.add(channel.getDisplayNumber()); + } + } + numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); + return numbers.size(); + } + + @VisibleForTesting + class FetchAsyncTask extends AsyncTask<Void, Void, Integer> { + private final JobService mService; + private final JobParameters mParams; + private Set<Channel> mCurrentChannels; + private TimerEvent mTimerEvent; + + private FetchAsyncTask(JobService service, JobParameters params) { + mService = service; + mParams = params; + } + + @Override + protected void onPreExecute() { + mTimerEvent = mPerformanceMonitor.startTimer(); + mCurrentChannels = new HashSet<>(mChannelDataManager.getChannelList()); + } + + @Override + protected Integer doInBackground(Void... args) { + final int oldTag = TrafficStats.getThreadStatsTag(); + TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH); + try { + if (DEBUG) Log.d(TAG, "Start EPG routinely fetching."); + Integer builtInResult = fetchEpgForBuiltInTuner(); + boolean anyCloudEpgFailure = false; + boolean anyCloudEpgSuccess = false; + return builtInResult; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + } + + private Set<Channel> getExistingChannelsFor(String inputId) { + Set<Channel> result = new HashSet<>(); + try (Cursor cursor = + mContext.getContentResolver() + .query( + TvContract.buildChannelsUriForInput(inputId), + ChannelImpl.PROJECTION, + null, + null, + null)) { + while (cursor.moveToNext()) { + result.add(ChannelImpl.fromCursor(cursor)); + } + return result; + } + } + + private Integer fetchEpgForBuiltInTuner() { + try { + Integer failureReason = prepareFetchEpg(false); + // InterruptedException might be caught by RPC, we should check it here. + if (failureReason != null || this.isCancelled()) { + return failureReason; + } + String lineupId = EpgFetchHelper.getLastLineupId(mContext); + lineupId = lineupId == null ? pickBestLineupId(mCurrentChannels) : lineupId; + if (lineupId != null) { + Log.i(TAG, "Selecting the lineup " + lineupId); + // During normal fetching process, the lineup ID should be confirmed since all + // channels are known, clear up possible lineups to save resources. + EpgFetchHelper.setLastLineupId(mContext, lineupId); + clearUnusedLineups(lineupId); + } else { + Log.i(TAG, "Failed to get lineup id"); + return REASON_NO_EPG_DATA_RETURNED; + } + Set<Channel> existingChannelsForMyPackage = + getExistingChannelsForMyPackage(mContext); + if (existingChannelsForMyPackage.isEmpty()) { + return REASON_NO_BUILT_IN_CHANNELS; + } + return fetchEpgFor(lineupId, existingChannelsForMyPackage); + } catch (Exception e) { + Log.w(TAG, "Failed to update EPG for builtin tuner", e); + return REASON_ERROR; + } + } + + @Nullable + private Integer fetchEpgFor(String lineupId, Set<Channel> existingChannels) { + if (DEBUG) { + Log.d( + TAG, + "Starting Fetching EPG is for " + + lineupId + + " with channelCount " + + existingChannels.size()); + } + final Set<EpgReader.EpgChannel> channels = + mEpgReader.getChannels(existingChannels, lineupId); + // InterruptedException might be caught by RPC, we should check it here. + if (this.isCancelled()) { + return null; + } + if (channels.isEmpty()) { + Log.i(TAG, "Failed to get EPG channels for " + lineupId); + return REASON_NO_EPG_DATA_RETURNED; + } + if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) + > mEpgDataExpiredTimeLimitMs) { + batchFetchEpg(channels, mFastFetchDurationSec); + } + new Handler(mContext.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + ChannelLogoFetcher.startFetchingChannelLogos( + mContext, asChannelList(channels)); + } + }); + for (EpgReader.EpgChannel epgChannel : channels) { + if (this.isCancelled()) { + return null; + } + List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(epgChannel)); + // InterruptedException might be caught by RPC, we should check it here. + Collections.sort(programs); + Log.i( + TAG, + "Fetched " + + programs.size() + + " programs for channel " + + epgChannel.getChannel()); + EpgFetchHelper.updateEpgData( + mContext, mClock, epgChannel.getChannel().getId(), programs); + } + EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp); + if (DEBUG) Log.d(TAG, "Fetching EPG is for " + lineupId); + return null; + } + + @Override + protected void onPostExecute(Integer failureReason) { + mFetchTask = null; + if (failureReason == null + || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED + || failureReason == REASON_NO_NEW_EPG) { + jobFinished(false); + } else { + // Applies back-off policy + jobFinished(true); + } + mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK); + mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK); + } + + @Override + protected void onCancelled(Integer failureReason) { + clearUnusedLineups(null); + jobFinished(false); + } + + private void jobFinished(boolean reschedule) { + if (mService != null && mParams != null) { + // Task is executed from JobService, need to report jobFinished. + mService.jobFinished(mParams, reschedule); + } + } + } + + private List<Channel> asChannelList(Set<EpgReader.EpgChannel> epgChannels) { + List<Channel> result = new ArrayList<>(epgChannels.size()); + for (EpgReader.EpgChannel epgChannel : epgChannels) { + result.add(epgChannel.getChannel()); + } + return result; + } + + @WorkerThread + private class FetchDuringScanHandler extends Handler { + private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>(); + private String mPossibleLineupId; + + private final ChannelDataManager.Listener mDuringScanChannelListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); + if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP + && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + Message.obtain( + FetchDuringScanHandler.this, + MSG_CHANNEL_UPDATED_DURING_SCAN, + getExistingChannelsForMyPackage(mContext)) + .sendToTarget(); + } + } + + @Override + public void onChannelListUpdated() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); + if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP + && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + Message.obtain( + FetchDuringScanHandler.this, + MSG_CHANNEL_UPDATED_DURING_SCAN, + getExistingChannelsForMyPackage(mContext)) + .sendToTarget(); + } + } + + @Override + public void onChannelBrowsableChanged() { + // Do nothing + } + }; + + @AnyThread + private FetchDuringScanHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PREPARE_FETCH_DURING_SCAN: + case MSG_RETRY_PREPARE_FETCH_DURING_SCAN: + onPrepareFetchDuringScan(); + break; + case MSG_CHANNEL_UPDATED_DURING_SCAN: + if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + onChannelUpdatedDuringScan((Set<Channel>) msg.obj); + } + break; + case MSG_FINISH_FETCH_DURING_SCAN: + removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN); + if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); + } else { + onFinishFetchDuringScan(); + } + break; + default: + // do nothing + } + } + + private void onPrepareFetchDuringScan() { + Integer failureReason = prepareFetchEpg(true); + if (failureReason != null) { + sendEmptyMessageDelayed( + MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS); + return; + } + mChannelDataManager.addListener(mDuringScanChannelListener); + } + + private void onChannelUpdatedDuringScan(Set<Channel> currentChannels) { + String lineupId = pickBestLineupId(currentChannels); + Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId); + if (TextUtils.isEmpty(lineupId)) { + if (TextUtils.isEmpty(mPossibleLineupId)) { + return; + } + } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) { + mFetchedChannelIdsDuringScan.clear(); + mPossibleLineupId = lineupId; + } + List<Long> currentChannelIds = new ArrayList<>(); + for (Channel channel : currentChannels) { + currentChannelIds.add(channel.getId()); + } + mFetchedChannelIdsDuringScan.retainAll(currentChannelIds); + Set<EpgReader.EpgChannel> newChannels = new HashSet<>(); + for (EpgReader.EpgChannel epgChannel : + mEpgReader.getChannels(currentChannels, mPossibleLineupId)) { + if (!mFetchedChannelIdsDuringScan.contains(epgChannel.getChannel().getId())) { + newChannels.add(epgChannel); + mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId()); + } + } + batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC); + } + + private void onFinishFetchDuringScan() { + mChannelDataManager.removeListener(mDuringScanChannelListener); + EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId); + clearUnusedLineups(null); + mFetchedChannelIdsDuringScan.clear(); + synchronized (mFetchDuringScanHandlerLock) { + if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) { + removeCallbacksAndMessages(null); + getLooper().quit(); + mFetchDuringScanHandler = null; + } + } + // Clear timestamp to make routine service start right away. + EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0); + Log.i(TAG, "EPG Fetching during channel scanning finished."); + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + fetchImmediately(); + } + }); + } + } +} diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java new file mode 100644 index 00000000..eada8b24 --- /dev/null +++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java @@ -0,0 +1,103 @@ +/* + * 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.data.epg; + +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.BuildConfig; +import com.android.tv.common.config.api.RemoteConfig; +import com.android.tv.common.experiments.Experiments; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Checks if a package or a input is white listed. */ +public final class EpgInputWhiteList { + private static final boolean DEBUG = false; + private static final String TAG = "EpgInputWhiteList"; + @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs"; + private static final String QA_DEV_INPUTS = + "com.example.partnersupportsampletvinput/.SampleTvInputService," + + "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService"; + + /** Returns the package portion of a inputId */ + @Nullable + public static String getPackageFromInput(@Nullable String inputId) { + return inputId == null ? null : inputId.substring(0, inputId.indexOf("/")); + } + + private final RemoteConfig remoteConfig; + + public EpgInputWhiteList(RemoteConfig remoteConfig) { + this.remoteConfig = remoteConfig; + } + + public boolean isInputWhiteListed(String inputId) { + return getWhiteListedInputs().contains(inputId); + } + + public boolean isPackageWhiteListed(String packageName) { + if (DEBUG) Log.d(TAG, "isPackageWhiteListed " + packageName); + Set<String> whiteList = getWhiteListedInputs(); + for (String good : whiteList) { + try { + String goodPackage = getPackageFromInput(good); + if (goodPackage.equals(packageName)) { + return true; + } + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "Error parsing package name of " + good, e); + continue; + } + } + return false; + } + + private Set<String> getWhiteListedInputs() { + Set<String> result = toInputSet(remoteConfig.getString(KEY)); + if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) { + HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS)); + if (result.isEmpty()) { + result = moreInputs; + } else { + result.addAll(moreInputs); + } + } + if (DEBUG) Log.d(TAG, "getWhiteListedInputs " + result); + return result; + } + + @VisibleForTesting + static Set<String> toInputSet(String value) { + if (TextUtils.isEmpty(value)) { + return Collections.emptySet(); + } + List<String> strings = Arrays.asList(value.split(",")); + Set<String> result = new HashSet<>(strings.size()); + for (String s : strings) { + String trimmed = s.trim(); + if (!TextUtils.isEmpty(trimmed)) { + result.add(trimmed); + } + } + return result; + } +} diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index c5aeca27..7147905a 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -19,28 +19,37 @@ package com.android.tv.data.epg; import android.support.annotation.AnyThread; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; - -import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.SeriesInfo; - +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; -/** - * An interface used to retrieve the EPG data. This class should be used in worker thread. - */ +/** An interface used to retrieve the EPG data. This class should be used in worker thread. */ @WorkerThread public interface EpgReader { - /** - * Checks if the reader is available. - */ + + /** Value class that holds a EpgChannelId and its corresponding {@link Channel} */ + // TODO(b/72052568): Get autovalue to work in aosp master + abstract class EpgChannel { + public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) { + return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId); + } + + public abstract Channel getChannel(); + + public abstract String getEpgChannelId(); + } + + /** Checks if the reader is available. */ boolean isAvailable(); /** - * Returns the timestamp of the current EPG. - * The format should be YYYYMMDDHHmmSS as a long value. ex) 20160308141500 + * Returns the timestamp of the current EPG. The format should be YYYYMMDDHHmmSS as a long + * value. ex) 20160308141500 */ long getEpgTimestamp(); @@ -61,31 +70,30 @@ public interface EpgReader { * Returns the list of channels for the given lineup. The returned channels should map into the * existing channels on the device. This method is usually called after selecting the lineup. */ - List<Channel> getChannels(@NonNull String lineupId); + Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId); /** Pre-loads and caches channels for a given lineup. */ void preloadChannels(@NonNull String lineupId); - /** - * Clears cached channels for a given lineup. - */ + /** Clears cached channels for a given lineup. */ @AnyThread void clearCachedChannels(@NonNull String lineupId); /** - * Returns the programs for the given channel. Must call {@link #getChannels(String)} + * Returns the programs for the given channel. Must call {@link #getChannels(Set, String)} * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not * retrieved from TvProvider. */ - List<Program> getPrograms(long channelId); + List<Program> getPrograms(EpgChannel epgChannel); /** * Returns the programs for the given channels. Note that the {@code Program} doesn't have valid * program ID because it's not retrieved from TvProvider. This method is only used to get * programs for a short duration typically. */ - Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration); + Map<EpgChannel, Collection<Program>> getPrograms( + @NonNull Set<EpgChannel> epgChannels, long duration); /** Returns the series information for the given series ID. */ SeriesInfo getSeriesInfo(@NonNull String seriesId); -}
\ No newline at end of file +} diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java index ab6935ad..3b001481 100644 --- a/src/com/android/tv/data/epg/StubEpgReader.java +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -17,23 +17,20 @@ package com.android.tv.data.epg; import android.content.Context; - import android.support.annotation.NonNull; -import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.SeriesInfo; - +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; -/** - * A stub class to read EPG. - */ -public class StubEpgReader implements EpgReader{ - public StubEpgReader(@SuppressWarnings("unused") Context context) { - } +/** A stub class to read EPG. */ +public class StubEpgReader implements EpgReader { + public StubEpgReader(@SuppressWarnings("unused") Context context) {} @Override public boolean isAvailable() { @@ -61,8 +58,8 @@ public class StubEpgReader implements EpgReader{ } @Override - public List<Channel> getChannels(@NonNull String lineupId) { - return Collections.emptyList(); + public Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId) { + return Collections.emptySet(); } @Override @@ -76,12 +73,13 @@ public class StubEpgReader implements EpgReader{ } @Override - public List<Program> getPrograms(long channelId) { + public List<Program> getPrograms(EpgChannel epgChannel) { return Collections.emptyList(); } @Override - public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) { + public Map<EpgChannel, Collection<Program>> getPrograms( + @NonNull Set<EpgChannel> channels, long duration) { return Collections.emptyMap(); } @@ -89,4 +87,4 @@ public class StubEpgReader implements EpgReader{ public SeriesInfo getSeriesInfo(@NonNull String seriesId) { return null; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java index d686e6e6..7e36591f 100644 --- a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java +++ b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java @@ -30,25 +30,21 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.RecordingState; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.List; -/** - * Displays the DVR history. - */ +/** Displays the DVR history. */ @TargetApi(VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public class DvrHistoryDialogFragment extends SafeDismissDialogFragment { public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName(); @@ -57,7 +53,7 @@ public class DvrHistoryDialogFragment extends SafeDismissDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); DvrDataManager dataManager = singletons.getDvrDataManager(); ChannelDataManager channelDataManager = singletons.getChannelDataManager(); for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) { @@ -67,60 +63,78 @@ public class DvrHistoryDialogFragment extends SafeDismissDialogFragment { } mSchedules.sort(ScheduledRecording.START_TIME_COMPARATOR.reversed()); LayoutInflater inflater = LayoutInflater.from(getContext()); - ArrayAdapter adapter = new ArrayAdapter<ScheduledRecording>(getContext(), - R.layout.list_item_dvr_history, ScheduledRecording.toArray(mSchedules)) { - @NonNull - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false); - ScheduledRecording schedule = mSchedules.get(position); - setText(view, R.id.state, getStateString(schedule.getState())); - setText(view, R.id.schedule_time, getRecordingTimeText(schedule)); - setText(view, R.id.program_title, DvrUiHelper.getStyledTitleWithEpisodeNumber( - getContext(), schedule, 0)); - setText(view, R.id.channel_name, getChannelNameText(schedule)); - return view; - } + ArrayAdapter adapter = + new ArrayAdapter<ScheduledRecording>( + getContext(), + R.layout.list_item_dvr_history, + ScheduledRecording.toArray(mSchedules)) { + @NonNull + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false); + ScheduledRecording schedule = mSchedules.get(position); + setText(view, R.id.state, getStateString(schedule.getState())); + setText(view, R.id.schedule_time, getRecordingTimeText(schedule)); + setText( + view, + R.id.program_title, + DvrUiHelper.getStyledTitleWithEpisodeNumber( + getContext(), schedule, 0)); + setText(view, R.id.channel_name, getChannelNameText(schedule)); + return view; + } - private void setText(View view, int id, CharSequence text) { - ((TextView) view.findViewById(id)).setText(text); - } + private void setText(View view, int id, CharSequence text) { + ((TextView) view.findViewById(id)).setText(text); + } - private void setText(View view, int id, int text) { - ((TextView) view.findViewById(id)).setText(text); - } + private void setText(View view, int id, int text) { + ((TextView) view.findViewById(id)).setText(text); + } - @SuppressLint("SwitchIntDef") - private int getStateString(@RecordingState int state) { - switch (state) { - case ScheduledRecording.STATE_RECORDING_CLIPPED: - return R.string.dvr_history_dialog_state_clip; - case ScheduledRecording.STATE_RECORDING_FAILED: - return R.string.dvr_history_dialog_state_fail; - case ScheduledRecording.STATE_RECORDING_FINISHED: - return R.string.dvr_history_dialog_state_success; - default: - break; - } - return 0; - } + @SuppressLint("SwitchIntDef") + private int getStateString(@RecordingState int state) { + switch (state) { + case ScheduledRecording.STATE_RECORDING_CLIPPED: + return R.string.dvr_history_dialog_state_clip; + case ScheduledRecording.STATE_RECORDING_FAILED: + return R.string.dvr_history_dialog_state_fail; + case ScheduledRecording.STATE_RECORDING_FINISHED: + return R.string.dvr_history_dialog_state_success; + default: + break; + } + return 0; + } - private String getChannelNameText(ScheduledRecording schedule) { - Channel channel = channelDataManager.getChannel(schedule.getChannelId()); - return channel == null ? null : - TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : - channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); - } + private String getChannelNameText(ScheduledRecording schedule) { + Channel channel = channelDataManager.getChannel(schedule.getChannelId()); + return channel == null + ? null + : TextUtils.isEmpty(channel.getDisplayName()) + ? channel.getDisplayNumber() + : channel.getDisplayName().trim() + + " " + + channel.getDisplayNumber(); + } - private String getRecordingTimeText(ScheduledRecording schedule) { - return Utils.getDurationString(getContext(), schedule.getStartTimeMs(), - schedule.getEndTimeMs(), true, true, true, 0); - } - }; + private String getRecordingTimeText(ScheduledRecording schedule) { + return Utils.getDurationString( + getContext(), + schedule.getStartTimeMs(), + schedule.getEndTimeMs(), + true, + true, + true, + 0); + } + }; ListView listView = new ListView(getActivity()); listView.setAdapter(adapter); - return new AlertDialog.Builder(getActivity()).setTitle(R.string.dvr_history_dialog_title) - .setView(listView).create(); + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.dvr_history_dialog_title) + .setView(listView) + .create(); } @Override diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java index d00422a7..53adb308 100644 --- a/src/com/android/tv/dialog/FullscreenDialogFragment.java +++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java @@ -23,21 +23,18 @@ import android.os.Bundle; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; - import com.android.tv.MainActivity; import com.android.tv.R; -/** - * Dialog fragment with full screen. - */ +/** Dialog fragment with full screen. */ public class FullscreenDialogFragment extends SafeDismissDialogFragment { public static final String DIALOG_TAG = FullscreenDialogFragment.class.getSimpleName(); public static final String VIEW_LAYOUT_ID = "viewLayoutId"; public static final String TRACKER_LABEL = "trackerLabel"; /** - * Creates a FullscreenDialogFragment. View class of viewLayoutResId should - * implement {@link DialogView}. + * Creates a FullscreenDialogFragment. View class of viewLayoutResId should implement {@link + * DialogView}. */ public static FullscreenDialogFragment newInstance(int viewLayoutResId, String trackerLabel) { FullscreenDialogFragment f = new FullscreenDialogFragment(); @@ -100,21 +97,13 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment { } } - /** - * Interface for the view of {@link FullscreenDialogFragment}. - */ + /** Interface for the view of {@link FullscreenDialogFragment}. */ public interface DialogView { - /** - * Called after the view is inflated and attached to the dialog. - */ + /** Called after the view is inflated and attached to the dialog. */ void initialize(MainActivity activity, Dialog dialog); - /** - * Called when a back key is pressed. - */ + /** Called when a back key is pressed. */ void onBackPressed(); - /** - * Called when {@link DialogFragment#onDestroy} is called. - */ + /** Called when {@link DialogFragment#onDestroy} is called. */ void onDestroy(); } } diff --git a/src/com/android/tv/dialog/HalfSizedDialogFragment.java b/src/com/android/tv/dialog/HalfSizedDialogFragment.java index 315c6a93..aed75655 100644 --- a/src/com/android/tv/dialog/HalfSizedDialogFragment.java +++ b/src/com/android/tv/dialog/HalfSizedDialogFragment.java @@ -24,9 +24,7 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; - import java.util.concurrent.TimeUnit; public class HalfSizedDialogFragment extends SafeDismissDialogFragment { @@ -38,16 +36,17 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { private OnActionClickListener mOnActionClickListener; private Handler mHandler = new Handler(); - private Runnable mAutoDismisser = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; + private Runnable mAutoDismisser = + new Runnable() { + @Override + public void run() { + dismiss(); + } + }; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.halfsized_dialog, container, false); } @@ -76,13 +75,14 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); + dialog.setOnKeyListener( + new DialogInterface.OnKeyListener() { + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { + mHandler.removeCallbacks(mAutoDismisser); + mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); + return false; + } + }); return dialog; } @@ -98,26 +98,24 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { /** * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog - * will be automatically closed when it's paused to prevent the fragment being re-created by - * the framework, which will result the listener being forgotten. + * will be automatically closed when it's paused to prevent the fragment being re-created by the + * framework, which will result the listener being forgotten. */ public void setOnActionClickListener(OnActionClickListener listener) { mOnActionClickListener = listener; } - /** - * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. - */ + /** Returns {@link OnActionClickListener} for sub-classes or any inner fragments. */ protected OnActionClickListener getOnActionClickListener() { return mOnActionClickListener; } /** * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments - * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier - * of the action user clicked. + * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier of + * the action user clicked. */ public interface OnActionClickListener { void onActionClick(long actionId); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java index d5c154da..71f45fbe 100644 --- a/src/com/android/tv/dialog/PinDialogFragment.java +++ b/src/com/android/tv/dialog/PinDialogFragment.java @@ -44,43 +44,34 @@ import android.view.ViewGroup.LayoutParams; import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.util.TvSettings; public class PinDialogFragment extends SafeDismissDialogFragment { private static final String TAG = "PinDialogFragment"; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; - /** - * PIN code dialog for unlock channel - */ + /** PIN code dialog for unlock channel */ public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0; /** - * PIN code dialog for unlock content. - * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title. + * PIN code dialog for unlock content. Only difference between {@code + * PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title. */ public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1; - /** - * PIN code dialog for change parental control settings - */ + /** PIN code dialog for change parental control settings */ public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2; - /** - * PIN code dialog for set new PIN - */ + /** PIN code dialog for set new PIN */ public static final int PIN_DIALOG_TYPE_NEW_PIN = 3; - // PIN code dialog for checking old PIN. This is internal only. + // PIN code dialog for checking old PIN. Only used in this class. private static final int PIN_DIALOG_TYPE_OLD_PIN = 4; - /** - * PIN code dialog for unlocking DVR playback - */ + /** PIN code dialog for unlocking DVR playback */ public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5; private static final int MAX_WRONG_PIN_COUNT = 5; @@ -94,7 +85,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { public static final String DIALOG_TAG = PinDialogFragment.class.getName(); private static final int NUMBER_PICKERS_RES_ID[] = { - R.id.first, R.id.second, R.id.third, R.id.fourth }; + R.id.first, R.id.second, R.id.third, R.id.fourth + }; private int mType; private int mRequestType; @@ -164,15 +156,16 @@ public class PinDialogFragment extends SafeDismissDialogFragment { // So apply view size to window after the DialogFragment.onStart() where dialog is shown. Dialog dlg = getDialog(); if (dlg != null) { - dlg.getWindow().setLayout( - getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), - LayoutParams.WRAP_CONTENT); + dlg.getWindow() + .setLayout( + getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), + LayoutParams.WRAP_CONTENT); } } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View v = inflater.inflate(R.layout.pin_dialog, container, false); mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin); @@ -199,7 +192,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment { mTitleView.setText( getString( R.string.pin_enter_unlock_dvr, - TvApplication.getSingletons(getContext()) + TvSingletons.getSingletons(getContext()) .getTvInputManagerHelper() .getContentRatingsManager() .getDisplayNameForRating(tvContentRating))); @@ -234,12 +227,13 @@ public class PinDialogFragment extends SafeDismissDialogFragment { return v; } - private final Runnable mUpdateEnterPinRunnable = new Runnable() { - @Override - public void run() { - updateWrongPin(); - } - }; + private final Runnable mUpdateEnterPinRunnable = + new Runnable() { + @Override + public void run() { + updateWrongPin(); + } + }; private void updateWrongPin() { if (getActivity() == null) { @@ -257,8 +251,12 @@ public class PinDialogFragment extends SafeDismissDialogFragment { } else { mEnterPinView.setVisibility(View.INVISIBLE); mWrongPinView.setVisibility(View.VISIBLE); - mWrongPinView.setText(getResources().getQuantityString(R.plurals.pin_enter_countdown, - remainingSeconds, remainingSeconds)); + mWrongPinView.setText( + getResources() + .getQuantityString( + R.plurals.pin_enter_countdown, + remainingSeconds, + remainingSeconds)); mHandler.postDelayed(mUpdateEnterPinRunnable, 1000); } } @@ -280,8 +278,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked); SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener); if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) { - ((OnPinCheckedListener) getActivity()).onPinChecked( - mPinChecked, mRequestType, mRatingString); + ((OnPinCheckedListener) getActivity()) + .onPinChecked(mPinChecked, mRequestType, mRatingString); } mDismissSilently = false; } @@ -391,7 +389,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { R.id.previous_number, R.id.current_number, R.id.next_number, - R.id.next2_number }; + R.id.next2_number + }; private static final int CURRENT_NUMBER_VIEW_INDEX = 2; private static final int NOT_INITIALIZED = Integer.MIN_VALUE; @@ -436,8 +435,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { this(context, attrs, defStyleAttr, 0); } - public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + public PinNumberPicker( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); View view = inflate(context, R.layout.pin_number_picker, this); mNumberViewHolder = view.findViewById(R.id.number_view_holder); @@ -447,118 +446,149 @@ public class PinDialogFragment extends SafeDismissDialogFragment { mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]); } Resources resources = context.getResources(); - mNumberViewHeight = resources.getDimensionPixelSize( - R.dimen.pin_number_picker_text_view_height); - - mNumberViewHolder.setOnFocusChangeListener(new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - updateFocus(true); - } - }); - - mNumberViewHolder.setOnKeyListener(new OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: { - if (mCancelAnimation) { - mScrollAnimatorSet.end(); + mNumberViewHeight = + resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height); + + mNumberViewHolder.setOnFocusChangeListener( + new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + updateFocus(true); + } + }); + + mNumberViewHolder.setOnKeyListener( + new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + { + if (mCancelAnimation) { + mScrollAnimatorSet.end(); + } + if (!mScrollAnimatorSet.isRunning()) { + mCancelAnimation = false; + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mNextValue = + adjustValueInValidRange( + mCurrentValue + 1); + startScrollAnimation(true); + } else { + mNextValue = + adjustValueInValidRange( + mCurrentValue - 1); + startScrollAnimation(false); + } + } + return true; + } } - if (!mScrollAnimatorSet.isRunning()) { - mCancelAnimation = false; - if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - mNextValue = adjustValueInValidRange(mCurrentValue + 1); - startScrollAnimation(true); - } else { - mNextValue = adjustValueInValidRange(mCurrentValue - 1); - startScrollAnimation(false); - } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + { + mCancelAnimation = true; + return true; + } } - return true; - } - } - } else if (event.getAction() == KeyEvent.ACTION_UP) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: { - mCancelAnimation = true; - return true; } + return false; } - } - return false; - } - }); + }); mNumberViewHolder.setScrollY(mNumberViewHeight); mFocusGainAnimator = new AnimatorSet(); mFocusGainAnimator.playTogether( - ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], - "alpha", 0f, sAlphaForAdjacentNumber), - ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX], - "alpha", sAlphaForFocusedNumber, 0f), - ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], - "alpha", 0f, sAlphaForAdjacentNumber), + ObjectAnimator.ofFloat( + mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], + "alpha", + 0f, + sAlphaForAdjacentNumber), + ObjectAnimator.ofFloat( + mNumberViews[CURRENT_NUMBER_VIEW_INDEX], + "alpha", + sAlphaForFocusedNumber, + 0f), + ObjectAnimator.ofFloat( + mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], + "alpha", + 0f, + sAlphaForAdjacentNumber), ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f)); - mFocusGainAnimator.setDuration(context.getResources().getInteger( - android.R.integer.config_shortAnimTime)); - mFocusGainAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(mBackgroundView.getText()); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber); - mBackgroundView.setText(""); - } - }); + mFocusGainAnimator.setDuration( + context.getResources().getInteger(android.R.integer.config_shortAnimTime)); + mFocusGainAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText( + mBackgroundView.getText()); + mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( + sAlphaForFocusedNumber); + mBackgroundView.setText(""); + } + }); mFocusLossAnimator = new AnimatorSet(); mFocusLossAnimator.playTogether( - ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], - "alpha", sAlphaForAdjacentNumber, 0f), - ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], - "alpha", sAlphaForAdjacentNumber, 0f), + ObjectAnimator.ofFloat( + mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], + "alpha", + sAlphaForAdjacentNumber, + 0f), + ObjectAnimator.ofFloat( + mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], + "alpha", + sAlphaForAdjacentNumber, + 0f), ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f)); - mFocusLossAnimator.setDuration(context.getResources().getInteger( - android.R.integer.config_shortAnimTime)); + mFocusLossAnimator.setDuration( + context.getResources().getInteger(android.R.integer.config_shortAnimTime)); mScrollAnimatorSet = new AnimatorSet(); - mScrollAnimatorSet.setDuration(context.getResources().getInteger( - R.integer.pin_number_scroll_duration)); - mScrollAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Set mCurrent value when scroll animation is finished. - mCurrentValue = mNextValue; - updateText(); - mNumberViewHolder.setScrollY(mNumberViewHeight); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber); - mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber); - } - }); + mScrollAnimatorSet.setDuration( + context.getResources().getInteger(R.integer.pin_number_scroll_duration)); + mScrollAnimatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Set mCurrent value when scroll animation is finished. + mCurrentValue = mNextValue; + updateText(); + mNumberViewHolder.setScrollY(mNumberViewHeight); + mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha( + sAlphaForAdjacentNumber); + mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha( + sAlphaForFocusedNumber); + mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha( + sAlphaForAdjacentNumber); + } + }); } static void loadResources(Context context) { if (sFocusedNumberEnterAnimator == null) { TypedValue outValue = new TypedValue(); - context.getResources().getValue( - R.dimen.pin_alpha_for_focused_number, outValue, true); + context.getResources() + .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true); sAlphaForFocusedNumber = outValue.getFloat(); - context.getResources().getValue( - R.dimen.pin_alpha_for_adjacent_number, outValue, true); + context.getResources() + .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true); sAlphaForAdjacentNumber = outValue.getFloat(); - sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context, - R.animator.pin_focused_number_enter); - sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context, - R.animator.pin_focused_number_exit); - sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context, - R.animator.pin_adjacent_number_enter); - sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context, - R.animator.pin_adjacent_number_exit); + sFocusedNumberEnterAnimator = + AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter); + sFocusedNumberExitAnimator = + AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit); + sAdjacentNumberEnterAnimator = + AnimatorInflater.loadAnimator( + context, R.animator.pin_adjacent_number_enter); + sAdjacentNumberExitAnimator = + AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit); } } @@ -588,15 +618,16 @@ public class PinDialogFragment extends SafeDismissDialogFragment { void startScrollAnimation(boolean scrollUp) { mFocusGainAnimator.end(); mFocusLossAnimator.end(); - final ValueAnimator scrollAnimator = ValueAnimator.ofInt( - 0, scrollUp ? mNumberViewHeight : -mNumberViewHeight); - scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - mNumberViewHolder.setScrollY(value + mNumberViewHeight); - } - }); + final ValueAnimator scrollAnimator = + ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight); + scrollAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int value = (Integer) animation.getAnimatedValue(); + mNumberViewHolder.setScrollY(value + mNumberViewHeight); + } + }); scrollAnimator.setDuration( getResources().getInteger(R.integer.pin_number_scroll_duration)); @@ -612,9 +643,12 @@ public class PinDialogFragment extends SafeDismissDialogFragment { sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); } - mScrollAnimatorSet.playTogether(scrollAnimator, - sAdjacentNumberExitAnimator, sFocusedNumberExitAnimator, - sFocusedNumberEnterAnimator, sAdjacentNumberEnterAnimator); + mScrollAnimatorSet.playTogether( + scrollAnimator, + sAdjacentNumberExitAnimator, + sFocusedNumberExitAnimator, + sFocusedNumberEnterAnimator, + sAdjacentNumberEnterAnimator); mScrollAnimatorSet.start(); } @@ -688,8 +722,10 @@ public class PinDialogFragment extends SafeDismissDialogFragment { // In order to show the text change animation, keep the text of // mNumberViews[CURRENT_NUMBER_VIEW_INDEX]. } else { - mNumberViews[i].setText(String.valueOf(adjustValueInValidRange( - mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i))); + mNumberViews[i].setText( + String.valueOf( + adjustValueInValidRange( + mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i))); } } } @@ -698,10 +734,11 @@ public class PinDialogFragment extends SafeDismissDialogFragment { private int adjustValueInValidRange(int value) { int interval = mMaxValue - mMinValue + 1; if (value < mMinValue - interval || value > mMaxValue + interval) { - throw new IllegalArgumentException("The value( " + value - + ") is too small or too big to adjust"); + throw new IllegalArgumentException( + "The value( " + value + ") is too small or too big to adjust"); } - return (value < mMinValue) ? value + interval + return (value < mMinValue) + ? value + interval : (value > mMaxValue) ? value - interval : value; } } @@ -714,8 +751,8 @@ public class PinDialogFragment extends SafeDismissDialogFragment { /** * Called when {@link PinDialogFragment} is dismissed. * - * @param checked {@code true} if the pin code entered is checked to be correct, - * otherwise {@code false}. + * @param checked {@code true} if the pin code entered is checked to be correct, otherwise + * {@code false}. * @param type The dialog type regarding to what pin entering is for. * @param rating The target rating to unblock for. */ diff --git a/src/com/android/tv/dialog/RecentlyWatchedDialogFragment.java b/src/com/android/tv/dialog/RecentlyWatchedDialogFragment.java index 1875f411..eb6940fb 100644 --- a/src/com/android/tv/dialog/RecentlyWatchedDialogFragment.java +++ b/src/com/android/tv/dialog/RecentlyWatchedDialogFragment.java @@ -29,17 +29,14 @@ import android.view.View; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; -/** - * Displays the watch history - */ -public class RecentlyWatchedDialogFragment extends SafeDismissDialogFragment implements - LoaderManager.LoaderCallbacks<Cursor> { +/** Displays the watch history */ +public class RecentlyWatchedDialogFragment extends SafeDismissDialogFragment + implements LoaderManager.LoaderCallbacks<Cursor> { public static final String DIALOG_TAG = RecentlyWatchedDialogFragment.class.getSimpleName(); private static final String EMPTY_STRING = ""; @@ -55,47 +52,57 @@ public class RecentlyWatchedDialogFragment extends SafeDismissDialogFragment imp ((MainActivity) getActivity()).getChannelDataManager(); String[] from = { - TvContract.WatchedPrograms._ID, - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, - TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_TITLE }; + TvContract.WatchedPrograms._ID, + TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, + TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, + TvContract.WatchedPrograms.COLUMN_TITLE + }; int[] to = { - R.id.watched_program_id, - R.id.watched_program_channel_id, - R.id.watched_program_watch_time, - R.id.watched_program_title}; - mAdapter = new SimpleCursorAdapter(getActivity(), R.layout.list_item_watched_program, null, - from, to, 0); - mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() { - @Override - public boolean setViewValue(View view, Cursor cursor, int columnIndex) { - String name = cursor.getColumnName(columnIndex); - if (TvContract.WatchedPrograms.COLUMN_CHANNEL_ID.equals(name)) { - long channelId = cursor.getLong(columnIndex); - ((TextView) view).setText(String.valueOf(channelId)); - // Update display number - String displayNumber; - Channel channel = dataChannelManager.getChannel(channelId); - if (channel == null) { - displayNumber = EMPTY_STRING; - } else { - displayNumber = channel.getDisplayNumber(); + R.id.watched_program_id, + R.id.watched_program_channel_id, + R.id.watched_program_watch_time, + R.id.watched_program_title + }; + mAdapter = + new SimpleCursorAdapter( + getActivity(), R.layout.list_item_watched_program, null, from, to, 0); + mAdapter.setViewBinder( + new SimpleCursorAdapter.ViewBinder() { + @Override + public boolean setViewValue(View view, Cursor cursor, int columnIndex) { + String name = cursor.getColumnName(columnIndex); + if (TvContract.WatchedPrograms.COLUMN_CHANNEL_ID.equals(name)) { + long channelId = cursor.getLong(columnIndex); + ((TextView) view).setText(String.valueOf(channelId)); + // Update display number + String displayNumber; + Channel channel = dataChannelManager.getChannel(channelId); + if (channel == null) { + displayNumber = EMPTY_STRING; + } else { + displayNumber = channel.getDisplayNumber(); + } + TextView displayNumberView = + ((TextView) + ((View) view.getParent()) + .findViewById( + R.id.watched_program_channel_display_number)); + displayNumberView.setText(displayNumber); + return true; + } else if (TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + .equals(name)) { + long time = cursor.getLong(columnIndex); + CharSequence timeString = + DateUtils.getRelativeTimeSpanString( + time, + System.currentTimeMillis(), + DateUtils.SECOND_IN_MILLIS); + ((TextView) view).setText(String.valueOf(timeString)); + return true; + } + return false; } - TextView displayNumberView = ((TextView) ((View) view.getParent()) - .findViewById(R.id.watched_program_channel_display_number)); - displayNumberView.setText(displayNumber); - return true; - } else if (TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS.equals( - name)) { - long time = cursor.getLong(columnIndex); - CharSequence timeString = DateUtils.getRelativeTimeSpanString(time, - System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS); - ((TextView) view).setText(String.valueOf(timeString)); - return true; - } - return false; - } - }); + }); ListView listView = new ListView(getActivity()); listView.setAdapter(mAdapter); @@ -121,12 +128,18 @@ public class RecentlyWatchedDialogFragment extends SafeDismissDialogFragment imp @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { String[] projection = { - TvContract.WatchedPrograms._ID, - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, - TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, - TvContract.WatchedPrograms.COLUMN_TITLE }; - return new CursorLoader(getActivity(), TvContract.WatchedPrograms.CONTENT_URI, projection, - null, null, TvContract.WatchedPrograms._ID + " DESC"); + TvContract.WatchedPrograms._ID, + TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, + TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS, + TvContract.WatchedPrograms.COLUMN_TITLE + }; + return new CursorLoader( + getActivity(), + TvContract.WatchedPrograms.CONTENT_URI, + projection, + null, + null, + TvContract.WatchedPrograms._ID + " DESC"); } @Override diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java index e3390b0a..6eb67dfd 100644 --- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java +++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java @@ -18,17 +18,13 @@ package com.android.tv.dialog; import android.app.Activity; import android.app.DialogFragment; - import com.android.tv.MainActivity; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.HasTrackerLabel; import com.android.tv.analytics.Tracker; -/** - * Provides the safe dismiss feature regardless of the DialogFragment's life cycle. - */ -public abstract class SafeDismissDialogFragment extends DialogFragment - implements HasTrackerLabel { +/** Provides the safe dismiss feature regardless of the DialogFragment's life cycle. */ +public abstract class SafeDismissDialogFragment extends DialogFragment implements HasTrackerLabel { private MainActivity mActivity; private boolean mAttached = false; private boolean mDismissPending = false; @@ -41,7 +37,7 @@ public abstract class SafeDismissDialogFragment extends DialogFragment if (activity instanceof MainActivity) { mActivity = (MainActivity) activity; } - mTracker = TvApplication.getSingletons(activity).getTracker(); + mTracker = TvSingletons.getSingletons(activity).getTracker(); if (mDismissPending) { mDismissPending = false; dismiss(); @@ -69,9 +65,7 @@ public abstract class SafeDismissDialogFragment extends DialogFragment mTracker = null; } - /** - * Dismiss safely regardless of the DialogFragment's life cycle. - */ + /** Dismiss safely regardless of the DialogFragment's life cycle. */ @Override public void dismiss() { if (!mAttached) { diff --git a/src/com/android/tv/dialog/WebDialogFragment.java b/src/com/android/tv/dialog/WebDialogFragment.java index 171a256b..5266f760 100644 --- a/src/com/android/tv/dialog/WebDialogFragment.java +++ b/src/com/android/tv/dialog/WebDialogFragment.java @@ -28,9 +28,7 @@ import android.view.ViewGroup.LayoutParams; import android.webkit.WebView; import android.webkit.WebViewClient; -/** - * A DialogFragment that shows a web view. - */ +/** A DialogFragment that shows a web view. */ public class WebDialogFragment extends SafeDismissDialogFragment { private static final String TAG = "WebDialogFragment"; private static final String URL = "URL"; @@ -43,11 +41,11 @@ public class WebDialogFragment extends SafeDismissDialogFragment { /** * Create a new WebDialogFragment to show a particular web page. * - * @param url The URL of the content to show. + * @param url The URL of the content to show. * @param title Optional title for the dialog. */ - public static WebDialogFragment newInstance(String url, @Nullable String title, - String trackerLabel) { + public static WebDialogFragment newInstance( + String url, @Nullable String title, String trackerLabel) { WebDialogFragment f = new WebDialogFragment(); Bundle args = new Bundle(); args.putString(URL, url); @@ -62,15 +60,17 @@ public class WebDialogFragment extends SafeDismissDialogFragment { super.onCreate(savedInstanceState); String title = getArguments().getString(TITLE); mTrackerLabel = getArguments().getString(TRACKER_LABEL); - int style = TextUtils.isEmpty(title) ? DialogFragment.STYLE_NO_TITLE - : DialogFragment.STYLE_NORMAL; + int style = + TextUtils.isEmpty(title) + ? DialogFragment.STYLE_NO_TITLE + : DialogFragment.STYLE_NORMAL; setStyle(style, 0); } @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { String title = getArguments().getString(TITLE); getDialog().setTitle(title); diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index a8637449..b8bffa18 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -23,15 +23,13 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.util.ArraySet; import android.util.Log; - import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.util.Clock; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.RecordingState; import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.util.Clock; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -42,14 +40,12 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -/** - * Base implementation of @{link DataManagerInternal}. - */ +/** Base implementation of @{link DataManagerInternal}. */ @MainThread @TargetApi(Build.VERSION_CODES.N) public abstract class BaseDvrDataManager implements WritableDvrDataManager { - private final static String TAG = "BaseDvrDataManager"; - private final static boolean DEBUG = false; + private static final String TAG = "BaseDvrDataManager"; + private static final boolean DEBUG = false; protected final Clock mClock; private final Set<OnDvrScheduleLoadFinishedListener> mOnDvrScheduleLoadFinishedListeners = @@ -61,7 +57,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { private final Set<RecordedProgramListener> mRecordedProgramListeners = new ArraySet<>(); private final HashMap<Long, ScheduledRecording> mDeletedScheduleMap = new HashMap<>(); - BaseDvrDataManager(Context context, Clock clock) { + public BaseDvrDataManager(Context context, Clock clock) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); mClock = clock; } @@ -129,8 +125,8 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } /** - * Calls {@link OnRecordedProgramLoadFinishedListener#onRecordedProgramLoadFinished()} - * for each listener. + * Calls {@link OnRecordedProgramLoadFinishedListener#onRecordedProgramLoadFinished()} for each + * listener. */ protected final void notifyRecordedProgramLoadFinished() { for (OnRecordedProgramLoadFinishedListener l : mOnRecordedProgramLoadFinishedListeners) { @@ -139,10 +135,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link RecordedProgramListener#onRecordedProgramsAdded} - * for each listener. - */ + /** Calls {@link RecordedProgramListener#onRecordedProgramsAdded} for each listener. */ protected final void notifyRecordedProgramsAdded(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(recordedPrograms)); @@ -150,10 +143,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link RecordedProgramListener#onRecordedProgramsChanged} - * for each listener. - */ + /** Calls {@link RecordedProgramListener#onRecordedProgramsChanged} for each listener. */ protected final void notifyRecordedProgramsChanged(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(recordedPrograms)); @@ -161,10 +151,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link RecordedProgramListener#onRecordedProgramsRemoved} - * for each listener. - */ + /** Calls {@link RecordedProgramListener#onRecordedProgramsRemoved} for each listener. */ protected final void notifyRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(recordedPrograms)); @@ -172,10 +159,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link SeriesRecordingListener#onSeriesRecordingAdded} - * for each listener. - */ + /** Calls {@link SeriesRecordingListener#onSeriesRecordingAdded} for each listener. */ protected final void notifySeriesRecordingAdded(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(seriesRecordings)); @@ -183,10 +167,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link SeriesRecordingListener#onSeriesRecordingRemoved} - * for each listener. - */ + /** Calls {@link SeriesRecordingListener#onSeriesRecordingRemoved} for each listener. */ protected final void notifySeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(seriesRecordings)); @@ -194,11 +175,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls - * {@link SeriesRecordingListener#onSeriesRecordingChanged} - * for each listener. - */ + /** Calls {@link SeriesRecordingListener#onSeriesRecordingChanged} for each listener. */ protected final void notifySeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(seriesRecordings)); @@ -206,10 +183,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} - * for each listener. - */ + /** Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. */ protected final void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(scheduledRecording)); @@ -217,10 +191,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } } - /** - * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} - * for each listener. - */ + /** Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener. */ protected final void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(scheduledRecording)); @@ -229,9 +200,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } /** - * Calls - * {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged} - * for each listener. + * Calls {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged} for each listener. */ protected final void notifyScheduledRecordingStatusChanged( ScheduledRecording... scheduledRecording) { @@ -257,28 +226,47 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { @Override public List<ScheduledRecording> getAvailableScheduledRecordings() { - return filterEndTimeIsPast(getRecordingsWithState( - ScheduledRecording.STATE_RECORDING_IN_PROGRESS, - ScheduledRecording.STATE_RECORDING_NOT_STARTED)); + return filterEndTimeIsPast( + getRecordingsWithState( + ScheduledRecording.STATE_RECORDING_IN_PROGRESS, + ScheduledRecording.STATE_RECORDING_NOT_STARTED)); } @Override public List<ScheduledRecording> getStartedRecordings() { - return filterEndTimeIsPast(getRecordingsWithState( - ScheduledRecording.STATE_RECORDING_IN_PROGRESS)); + return filterEndTimeIsPast( + getRecordingsWithState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)); } @Override public List<ScheduledRecording> getNonStartedScheduledRecordings() { - return filterEndTimeIsPast(getRecordingsWithState( - ScheduledRecording.STATE_RECORDING_NOT_STARTED)); + return filterEndTimeIsPast( + getRecordingsWithState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)); + } + + @Override + public List<ScheduledRecording> getFailedScheduledRecordings() { + return getRecordingsWithState(ScheduledRecording.STATE_RECORDING_FAILED); } @Override public void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState) { if (scheduledRecording.getState() != newState) { - updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording) - .setState(newState).build()); + updateScheduledRecording( + ScheduledRecording.buildFrom(scheduledRecording).setState(newState).build()); + } + } + + @Override + public void changeState( + ScheduledRecording scheduledRecording, @RecordingState int newState, int reason) { + if (scheduledRecording.getState() != newState) { + ScheduledRecording.Builder builder = + ScheduledRecording.buildFrom(scheduledRecording).setState(newState); + if (newState == ScheduledRecording.STATE_RECORDING_FAILED) { + builder.setFailedReason(reason); + } + updateScheduledRecording(builder.build()); } } @@ -300,9 +288,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { return mDeletedScheduleMap; } - /** - * Returns the schedules whose state is contained by states. - */ + /** Returns the schedules whose state is contained by states. */ protected abstract List<ScheduledRecording> getRecordingsWithState(int... states); @Override @@ -357,5 +343,5 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override - public void forgetStorage(String inputId) { } + public void forgetStorage(String inputId) {} } diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 6d400b82..10dfc4c9 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -20,48 +20,36 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Range; - import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.RecordingState; import com.android.tv.dvr.data.SeriesRecording; - import java.util.Collection; import java.util.List; -/** - * Read only data manager. - */ +/** Read only data manager. */ @MainThread public interface DvrDataManager { long NEXT_START_TIME_NOT_FOUND = -1; boolean isInitialized(); - /** - * Returns {@code true} if the schedules were loaded, otherwise {@code false}. - */ + /** Returns {@code true} if the schedules were loaded, otherwise {@code false}. */ boolean isDvrScheduleLoadFinished(); - /** - * Returns {@code true} if the recorded programs were loaded, otherwise {@code false}. - */ + /** Returns {@code true} if the recorded programs were loaded, otherwise {@code false}. */ boolean isRecordedProgramLoadFinished(); - /** - * Returns past recordings. - */ + /** Returns past recordings. */ List<RecordedProgram> getRecordedPrograms(); - /** - * Returns past recorded programs in the given series. - */ + /** Returns past recorded programs in the given series. */ List<RecordedProgram> getRecordedPrograms(long seriesRecordingId); /** * Returns all {@link ScheduledRecording} regardless of state. - * <p> - * The result doesn't contain the deleted schedules. + * + * <p>The result doesn't contain the deleted schedules. */ List<ScheduledRecording> getAllScheduledRecordings(); @@ -71,29 +59,24 @@ public interface DvrDataManager { */ List<ScheduledRecording> getAvailableScheduledRecordings(); - /** - * Returns started recordings that expired. - */ + /** Returns started recordings that expired. */ List<ScheduledRecording> getStartedRecordings(); - /** - * Returns scheduled but not started recordings that have not expired. - */ + /** Returns scheduled but not started recordings that have not expired. */ List<ScheduledRecording> getNonStartedScheduledRecordings(); - /** - * Returns series recordings. - */ + /** Returns failed recordings. */ + List<ScheduledRecording> getFailedScheduledRecordings(); + + /** Returns series recordings. */ List<SeriesRecording> getSeriesRecordings(); - /** - * Returns series recordings from the given input. - */ + /** Returns series recordings from the given input. */ List<SeriesRecording> getSeriesRecordings(String inputId); /** - * Returns the next start time after {@code time} or {@link #NEXT_START_TIME_NOT_FOUND} - * if none is found. + * Returns the next start time after {@code time} or {@link #NEXT_START_TIME_NOT_FOUND} if none + * is found. * * @param time time milliseconds */ @@ -103,73 +86,48 @@ public interface DvrDataManager { * Returns a list of the schedules with a overlap with the given time period inclusive and with * the given state. * - * <p> A recording overlaps with a period when - * {@code recording.getStartTime() <= period.getUpper() && - * recording.getEndTime() >= period.getLower()}. + * <p>A recording overlaps with a period when {@code recording.getStartTime() <= + * period.getUpper() && recording.getEndTime() >= period.getLower()}. * * @param period a time period in milliseconds. * @param state the state of the schedule. */ List<ScheduledRecording> getScheduledRecordings(Range<Long> period, @RecordingState int state); - /** - * Returns a list of the schedules in the given series. - */ + /** Returns a list of the schedules in the given series. */ List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId); - /** - * Returns a list of the schedules from the given input. - */ + /** Returns a list of the schedules from the given input. */ List<ScheduledRecording> getScheduledRecordings(String inputId); - /** - * Add a {@link OnDvrScheduleLoadFinishedListener}. - */ + /** Add a {@link OnDvrScheduleLoadFinishedListener}. */ void addDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener); - /** - * Remove a {@link OnDvrScheduleLoadFinishedListener}. - */ + /** Remove a {@link OnDvrScheduleLoadFinishedListener}. */ void removeDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener); - /** - * Add a {@link OnRecordedProgramLoadFinishedListener}. - */ + /** Add a {@link OnRecordedProgramLoadFinishedListener}. */ void addRecordedProgramLoadFinishedListener(OnRecordedProgramLoadFinishedListener listener); - /** - * Remove a {@link OnRecordedProgramLoadFinishedListener}. - */ + /** Remove a {@link OnRecordedProgramLoadFinishedListener}. */ void removeRecordedProgramLoadFinishedListener(OnRecordedProgramLoadFinishedListener listener); - /** - * Add a {@link ScheduledRecordingListener}. - */ + /** Add a {@link ScheduledRecordingListener}. */ void addScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener); - /** - * Remove a {@link ScheduledRecordingListener}. - */ + /** Remove a {@link ScheduledRecordingListener}. */ void removeScheduledRecordingListener(ScheduledRecordingListener scheduledRecordingListener); - /** - * Add a {@link RecordedProgramListener}. - */ + /** Add a {@link RecordedProgramListener}. */ void addRecordedProgramListener(RecordedProgramListener listener); - /** - * Remove a {@link RecordedProgramListener}. - */ + /** Remove a {@link RecordedProgramListener}. */ void removeRecordedProgramListener(RecordedProgramListener listener); - /** - * Add a {@link ScheduledRecordingListener}. - */ + /** Add a {@link ScheduledRecordingListener}. */ void addSeriesRecordingListener(SeriesRecordingListener seriesRecordingListener); - /** - * Remove a {@link ScheduledRecordingListener}. - */ + /** Remove a {@link ScheduledRecordingListener}. */ void removeSeriesRecordingListener(SeriesRecordingListener seriesRecordingListener); /** @@ -178,65 +136,47 @@ public interface DvrDataManager { @Nullable ScheduledRecording getScheduledRecording(long recordingId); - /** - * Returns the scheduled recording program with the given programId or null if is not found. - */ + /** Returns the scheduled recording program with the given programId or null if is not found. */ @Nullable ScheduledRecording getScheduledRecordingForProgramId(long programId); - /** - * Returns the recorded program with the given recordingId or null if is not found. - */ + /** Returns the recorded program with the given recordingId or null if is not found. */ @Nullable RecordedProgram getRecordedProgram(long recordingId); - /** - * Returns the series recording with the given seriesId or null if is not found. - */ + /** Returns the series recording with the given seriesId or null if is not found. */ @Nullable SeriesRecording getSeriesRecording(long seriesRecordingId); - /** - * Returns the series recording with the given series ID or {@code null} if not found. - */ + /** Returns the series recording with the given series ID or {@code null} if not found. */ @Nullable SeriesRecording getSeriesRecording(String seriesId); - /** - * Returns the schedules which are marked deleted. - */ + /** Returns the schedules which are marked deleted. */ Collection<ScheduledRecording> getDeletedSchedules(); - /** - * Returns the program IDs which is not allowed to make a schedule automatically. - */ + /** Returns the program IDs which is not allowed to make a schedule automatically. */ @NonNull Collection<Long> getDisallowedProgramIds(); /** - * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains - * any available schedules or recorded programs, and it's status is - * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. + * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains any + * available schedules or recorded programs, and it's status is {@link + * SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. */ void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds); - /** - * Listens for the DVR schedules loading finished. - */ + /** Listens for the DVR schedules loading finished. */ interface OnDvrScheduleLoadFinishedListener { void onDvrScheduleLoadFinished(); } - /** - * Listens for the recorded program loading finished. - */ + /** Listens for the recorded program loading finished. */ interface OnRecordedProgramLoadFinishedListener { void onRecordedProgramLoadFinished(); } - /** - * Listens for changes to {@link ScheduledRecording}s. - */ + /** Listens for changes to {@link ScheduledRecording}s. */ interface ScheduledRecordingListener { void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings); @@ -250,9 +190,7 @@ public interface DvrDataManager { void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings); } - /** - * Listens for changes to {@link SeriesRecording}s. - */ + /** Listens for changes to {@link SeriesRecording}s. */ interface SeriesRecordingListener { void onSeriesRecordingAdded(SeriesRecording... seriesRecordings); @@ -261,9 +199,7 @@ public interface DvrDataManager { void onSeriesRecordingChanged(SeriesRecording... seriesRecordings); } - /** - * Listens for changes to {@link RecordedProgram}s. - */ + /** Listens for changes to {@link RecordedProgram}s. */ interface RecordedProgramListener { void onRecordedProgramsAdded(RecordedProgram... recordedPrograms); diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 6094ca72..2b4ecbf5 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -38,10 +38,12 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Range; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener; +import com.android.tv.common.util.Clock; +import com.android.tv.common.util.CommonUtils; import com.android.tv.dvr.data.IdGenerator; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; @@ -59,12 +61,9 @@ import com.android.tv.dvr.provider.DvrDbSync; import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; -import com.android.tv.util.Clock; import com.android.tv.util.Filter; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvUriMatcher; -import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -73,10 +72,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.Executor; -/** - * DVR Data manager to handle recordings and schedules. - */ +/** DVR Data manager to handle recordings and schedules. */ @MainThread @TargetApi(Build.VERSION_CODES.N) public class DvrDataManagerImpl extends BaseDvrDataManager { @@ -98,52 +96,54 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>(); private final Context mContext; - private final ContentObserver mContentObserver = new ContentObserver(new Handler( - Looper.getMainLooper())) { - @Override - public void onChange(boolean selfChange) { - onChange(selfChange, null); - } + private Executor mDbExecutor; + private final ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } - @Override - public void onChange(boolean selfChange, final @Nullable Uri uri) { - RecordedProgramsQueryTask task = new RecordedProgramsQueryTask( - mContext.getContentResolver(), uri); - task.executeOnDbThread(); - mPendingTasks.add(task); - } - }; + @Override + public void onChange(boolean selfChange, final @Nullable Uri uri) { + RecordedProgramsQueryTask task = + new RecordedProgramsQueryTask(mContext.getContentResolver(), uri); + task.executeOnDbThread(); + mPendingTasks.add(task); + } + }; private boolean mDvrLoadFinished; private boolean mRecordedProgramLoadFinished; private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); private DvrDbSync mDbSync; - private DvrStorageStatusManager mStorageStatusManager; + private RecordingStorageStatusManager mStorageStatusManager; - private final TvInputCallback mInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); - if (!isInputAvailable(inputId)) { - if (DEBUG) Log.d(TAG, "Not available for recording"); - return; - } - unhideInput(inputId); - } + private final TvInputCallback mInputCallback = + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); + if (!isInputAvailable(inputId)) { + if (DEBUG) Log.d(TAG, "Not available for recording"); + return; + } + unhideInput(inputId); + } - @Override - public void onInputRemoved(String inputId) { - if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); - hideInput(inputId); - } - }; + @Override + public void onInputRemoved(String inputId) { + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); + hideInput(inputId); + } + }; private final OnStorageMountChangedListener mStorageMountChangedListener = new OnStorageMountChangedListener() { @Override public void onStorageMountChanged(boolean storageMounted) { for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) { - if (Utils.isBundledInput(input.getId())) { + if (CommonUtils.isBundledInput(input.getId())) { if (storageMounted) { unhideInput(input.getId()); } else { @@ -154,8 +154,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } }; - private static <T> List<T> moveElements(HashMap<Long, T> from, HashMap<Long, T> to, - Filter<T> filter) { + private static <T> List<T> moveElements( + HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) { List<T> moved = new ArrayList<>(); Iterator<Entry<Long, T>> iter = from.entrySet().iterator(); while (iter.hasNext()) { @@ -172,119 +172,139 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public DvrDataManagerImpl(Context context, Clock clock) { super(context, clock); mContext = context; - mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); - mStorageStatusManager = TvApplication.getSingletons(context).getDvrStorageStatusManager(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mInputManager = tvSingletons.getTvInputManagerHelper(); + mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager(); + mDbExecutor = tvSingletons.getDbExecutor(); } public void start() { mInputManager.addCallback(mInputCallback); mStorageStatusManager.addListener(mStorageMountChangedListener); - AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask - = new AsyncDvrQuerySeriesRecordingTask(mContext) { - @Override - protected void onCancelled(List<SeriesRecording> seriesRecordings) { - mPendingTasks.remove(this); - } + AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask = + new AsyncDvrQuerySeriesRecordingTask(mContext) { + @Override + protected void onCancelled(List<SeriesRecording> seriesRecordings) { + mPendingTasks.remove(this); + } - @Override - protected void onPostExecute(List<SeriesRecording> seriesRecordings) { - mPendingTasks.remove(this); - long maxId = 0; - HashSet<String> seriesIds = new HashSet<>(); - for (SeriesRecording r : seriesRecordings) { - if (SoftPreconditions.checkState(!seriesIds.contains(r.getSeriesId()), TAG, - "Skip loading series recording with duplicate series ID: " + r)) { - seriesIds.add(r.getSeriesId()); - if (isInputAvailable(r.getInputId())) { - mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); - } else { - mSeriesRecordingsForRemovedInput.put(r.getId(), r); + @Override + protected void onPostExecute(List<SeriesRecording> seriesRecordings) { + mPendingTasks.remove(this); + long maxId = 0; + HashSet<String> seriesIds = new HashSet<>(); + for (SeriesRecording r : seriesRecordings) { + if (SoftPreconditions.checkState( + !seriesIds.contains(r.getSeriesId()), + TAG, + "Skip loading series recording with duplicate series ID: " + + r)) { + seriesIds.add(r.getSeriesId()); + if (isInputAvailable(r.getInputId())) { + mSeriesRecordings.put(r.getId(), r); + mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + } else { + mSeriesRecordingsForRemovedInput.put(r.getId(), r); + } + } + if (maxId < r.getId()) { + maxId = r.getId(); + } } + IdGenerator.SERIES_RECORDING.setMaxId(maxId); } - if (maxId < r.getId()) { - maxId = r.getId(); - } - } - IdGenerator.SERIES_RECORDING.setMaxId(maxId); - } - }; + }; dvrQuerySeriesRecordingTask.executeOnDbThread(); mPendingTasks.add(dvrQuerySeriesRecordingTask); - AsyncDvrQueryScheduleTask dvrQueryScheduleTask - = new AsyncDvrQueryScheduleTask(mContext) { - @Override - protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { - mPendingTasks.remove(this); - } + AsyncDvrQueryScheduleTask dvrQueryScheduleTask = + new AsyncDvrQueryScheduleTask(mContext) { + @Override + protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { + mPendingTasks.remove(this); + } - @SuppressLint("SwitchIntDef") - @Override - protected void onPostExecute(List<ScheduledRecording> result) { - mPendingTasks.remove(this); - long maxId = 0; - List<SeriesRecording> seriesRecordingsToAdd = new ArrayList<>(); - List<ScheduledRecording> toUpdate = new ArrayList<>(); - List<ScheduledRecording> toDelete = new ArrayList<>(); - for (ScheduledRecording r : result) { - if (!isInputAvailable(r.getInputId())) { - mScheduledRecordingsForRemovedInput.put(r.getId(), r); - } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { - getDeletedScheduleMap().put(r.getProgramId(), r); - } else { - mScheduledRecordings.put(r.getId(), r); - if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { - mProgramId2ScheduledRecordings.put(r.getProgramId(), r); - } - // Adjust the state of the schedules before DB loading is finished. - switch (r.getState()) { - case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); - } else { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState( - ScheduledRecording.STATE_RECORDING_NOT_STARTED) - .build()); + @SuppressLint("SwitchIntDef") + @Override + protected void onPostExecute(List<ScheduledRecording> result) { + mPendingTasks.remove(this); + long maxId = 0; + int reasonNotStarted = + ScheduledRecording + .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; + List<ScheduledRecording> toUpdate = new ArrayList<>(); + List<ScheduledRecording> toDelete = new ArrayList<>(); + for (ScheduledRecording r : result) { + if (!isInputAvailable(r.getInputId())) { + mScheduledRecordingsForRemovedInput.put(r.getId(), r); + } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { + getDeletedScheduleMap().put(r.getProgramId(), r); + } else { + mScheduledRecordings.put(r.getId(), r); + if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { + mProgramId2ScheduledRecordings.put(r.getProgramId(), r); } - break; - case ScheduledRecording.STATE_RECORDING_NOT_STARTED: - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); + // Adjust the state of the schedules before DB loading is finished. + switch (r.getState()) { + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + int reason = + ScheduledRecording.FAILED_REASON_NOT_FINISHED; + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_FAILED) + .setFailedReason(reason) + .build()); + } else { + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_NOT_STARTED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording + .STATE_RECORDING_FAILED) + .setFailedReason(reasonNotStarted) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_CANCELED: + toDelete.add(r); + break; + default: // fall out } - break; - case ScheduledRecording.STATE_RECORDING_CANCELED: - toDelete.add(r); - break; + } + if (maxId < r.getId()) { + maxId = r.getId(); + } + } + if (!toUpdate.isEmpty()) { + updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); + } + if (!toDelete.isEmpty()) { + removeScheduledRecording(ScheduledRecording.toArray(toDelete)); + } + IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); + if (mRecordedProgramLoadFinished) { + validateSeriesRecordings(); + } + mDvrLoadFinished = true; + notifyDvrScheduleLoadFinished(); + if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); + SeriesRecordingScheduler.getInstance(mContext).start(); } } - if (maxId < r.getId()) { - maxId = r.getId(); - } - } - if (!toUpdate.isEmpty()) { - updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); - } - if (!toDelete.isEmpty()) { - removeScheduledRecording(ScheduledRecording.toArray(toDelete)); - } - IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); - if (mRecordedProgramLoadFinished) { - validateSeriesRecordings(); - } - mDvrLoadFinished = true; - notifyDvrScheduleLoadFinished(); - if (isInitialized()) { - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); - SeriesRecordingScheduler.getInstance(mContext).start(); - } - } - }; + }; dvrQueryScheduleTask.executeOnDbThread(); mPendingTasks.add(dvrQueryScheduleTask); RecordedProgramsQueryTask mRecordedProgramQueryTask = @@ -341,8 +361,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mRecordedProgramsForRemovedInput.clear(); notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms)); } else { - HashMap<Long, RecordedProgram> oldRecordedPrograms - = new HashMap<>(mRecordedPrograms); + HashMap<Long, RecordedProgram> oldRecordedPrograms = + new HashMap<>(mRecordedPrograms); mRecordedPrograms.clear(); mRecordedProgramsForRemovedInput.clear(); List<RecordedProgram> addedRecordedPrograms = new ArrayList<>(); @@ -492,7 +512,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } @VisibleForTesting - static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) { + static long getNextStartTimeAfter( + List<ScheduledRecording> scheduledRecordings, long startTime) { int start = 0; int end = scheduledRecordings.size() - 1; while (start <= end) { @@ -503,13 +524,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { end = mid - 1; } } - return start < scheduledRecordings.size() ? scheduledRecordings.get(start).getStartTimeMs() + return start < scheduledRecordings.size() + ? scheduledRecordings.get(start).getStartTimeMs() : NEXT_START_TIME_NOT_FOUND; } @Override - public List<ScheduledRecording> getScheduledRecordings(Range<Long> period, - @RecordingState int state) { + public List<ScheduledRecording> getScheduledRecordings( + Range<Long> period, @RecordingState int state) { List<ScheduledRecording> result = new ArrayList<>(); for (ScheduledRecording r : mScheduledRecordings.values()) { if (r.isOverLapping(period) && r.getState() == state) { @@ -595,8 +617,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { r.setId(IdGenerator.SERIES_RECORDING.newId()); mSeriesRecordings.put(r.getId(), r); SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); - SoftPreconditions.checkArgument(previousSeries == null, TAG, "Attempt to add series" - + " recording with the duplicate series ID: " + r.getSeriesId()); + SoftPreconditions.checkArgument( + previousSeries == null, + TAG, + "Attempt to add series" + " recording with the duplicate series ID: %s", + r.getSeriesId()); } if (mDvrLoadFinished) { notifySeriesRecordingAdded(seriesRecordings); @@ -620,20 +645,23 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mProgramId2ScheduledRecordings.remove(r.getProgramId()); if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); } boolean isScheduleForRemovedInput = mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; // If it belongs to the series recording and it's not started yet, just mark delete // instead of deleting it. - if (!isScheduleForRemovedInput && !forceRemove + if (!isScheduleForRemovedInput + && !forceRemove && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) { + || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) { SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET); - ScheduledRecording deleted = ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_DELETED).build(); + ScheduledRecording deleted = + ScheduledRecording.buildFrom(r) + .setState(ScheduledRecording.STATE_RECORDING_DELETED) + .build(); getDeletedScheduleMap().put(deleted.getProgramId(), deleted); schedulesNotToDelete.add(deleted); } else { @@ -655,12 +683,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext).executeOnDbThread( - ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteScheduleTask(mContext) + .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); } if (!schedulesNotToDelete.isEmpty()) { - new AsyncUpdateScheduleTask(mContext).executeOnDbThread( - ScheduledRecording.toArray(schedulesNotToDelete)); + new AsyncUpdateScheduleTask(mContext) + .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete)); } } @@ -680,8 +708,10 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { toDelete.add(r); } else { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setSeriesRecordingId(SeriesRecording.ID_NOT_SET).build()); + toUpdate.add( + ScheduledRecording.buildFrom(r) + .setSeriesRecordingId(SeriesRecording.ID_NOT_SET) + .build()); } } } @@ -709,7 +739,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { List<ScheduledRecording> toUpdate = new ArrayList<>(); Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { - if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, + if (!SoftPreconditions.checkState( + mScheduledRecordings.containsKey(r.getId()), + TAG, "Recording not found for: " + r)) { continue; } @@ -720,8 +752,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { long programId = r.getProgramId(); if (oldScheduledRecording.getProgramId() != programId && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { - ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings - .get(oldScheduledRecording.getProgramId()); + ScheduledRecording oldValueForProgramId = + mProgramId2ScheduledRecordings.get(oldScheduledRecording.getProgramId()); if (oldValueForProgramId.getId() == r.getId()) { // Only remove the old ScheduledRecording if it has the same ID as the new one. mProgramId2ScheduledRecordings.remove(oldScheduledRecording.getProgramId()); @@ -755,14 +787,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { - if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG, - "Non Existing Series ID: " + r)) { + if (!SoftPreconditions.checkArgument( + mSeriesRecordings.containsKey(r.getId()), + TAG, + "Non Existing Series ID: %s", + r)) { continue; } SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); - SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" - + " updated: " + r); + SoftPreconditions.checkArgument( + old1.equals(old2), TAG, "Series ID cannot be updated: %s", r); } if (mDvrLoadFinished) { notifySeriesRecordingChanged(seriesRecordings); @@ -772,7 +807,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private boolean isInputAvailable(String inputId) { return mInputManager.hasTvInputInfo(inputId) - && (!Utils.isBundledInput(inputId) || mStorageStatusManager.isStorageMounted()); + && (!CommonUtils.isBundledInput(inputId) + || mStorageStatusManager.isStorageMounted()); } private void removeDeletedSchedules(ScheduledRecording... addedSchedules) { @@ -784,8 +820,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext).executeOnDbThread( - ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteScheduleTask(mContext) + .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); } } @@ -805,15 +841,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!schedulesToDelete.isEmpty()) { - new AsyncDeleteScheduleTask(mContext).executeOnDbThread( - ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteScheduleTask(mContext) + .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); } } private void unhideInput(String inputId) { if (DEBUG) Log.d(TAG, "unhideInput " + inputId); List<ScheduledRecording> movedSchedules = - moveElements(mScheduledRecordingsForRemovedInput, mScheduledRecordings, + moveElements( + mScheduledRecordingsForRemovedInput, + mScheduledRecordings, new Filter<ScheduledRecording>() { @Override public boolean filter(ScheduledRecording r) { @@ -821,7 +859,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } }); List<RecordedProgram> movedRecordedPrograms = - moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, + moveElements( + mRecordedProgramsForRemovedInput, + mRecordedPrograms, new Filter<RecordedProgram>() { @Override public boolean filter(RecordedProgram r) { @@ -830,7 +870,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }); List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); List<SeriesRecording> movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + moveElements( + mSeriesRecordingsForRemovedInput, + mSeriesRecordings, new Filter<SeriesRecording>() { @Override public boolean filter(SeriesRecording r) { @@ -856,8 +898,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { for (SeriesRecording r : removedSeriesRecordings) { mSeriesRecordingsForRemovedInput.remove(r.getId()); } - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( - SeriesRecording.toArray(removedSeriesRecordings)); + new AsyncDeleteSeriesRecordingTask(mContext) + .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings)); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -873,7 +915,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private void hideInput(String inputId) { if (DEBUG) Log.d(TAG, "hideInput " + inputId); List<ScheduledRecording> movedSchedules = - moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, + moveElements( + mScheduledRecordings, + mScheduledRecordingsForRemovedInput, new Filter<ScheduledRecording>() { @Override public boolean filter(ScheduledRecording r) { @@ -881,7 +925,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } }); List<SeriesRecording> movedSeriesRecordings = - moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, + moveElements( + mSeriesRecordings, + mSeriesRecordingsForRemovedInput, new Filter<SeriesRecording>() { @Override public boolean filter(SeriesRecording r) { @@ -889,7 +935,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } }); List<RecordedProgram> movedRecordedPrograms = - moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, + moveElements( + mRecordedPrograms, + mRecordedProgramsForRemovedInput, new Filter<RecordedProgram>() { @Override public boolean filter(RecordedProgram r) { @@ -931,7 +979,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void forgetStorage(String inputId) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); for (Iterator<ScheduledRecording> i = - mScheduledRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + mScheduledRecordingsForRemovedInput.values().iterator(); + i.hasNext(); ) { ScheduledRecording r = i.next(); if (inputId.equals(r.getInputId())) { schedulesToDelete.add(r); @@ -939,32 +988,34 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>(); - for (Iterator<SeriesRecording> i = - mSeriesRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + for (Iterator<SeriesRecording> i = mSeriesRecordingsForRemovedInput.values().iterator(); + i.hasNext(); ) { SeriesRecording r = i.next(); if (inputId.equals(r.getInputId())) { seriesRecordingsToDelete.add(r); i.remove(); } } - for (Iterator<RecordedProgram> i = - mRecordedProgramsForRemovedInput.values().iterator(); i.hasNext(); ) { + for (Iterator<RecordedProgram> i = mRecordedProgramsForRemovedInput.values().iterator(); + i.hasNext(); ) { if (inputId.equals(i.next().getInputId())) { i.remove(); } } - new AsyncDeleteScheduleTask(mContext).executeOnDbThread( - ScheduledRecording.toArray(schedulesToDelete)); - new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( - SeriesRecording.toArray(seriesRecordingsToDelete)); - new AsyncDbTask<Void, Void, Void>() { + new AsyncDeleteScheduleTask(mContext) + .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteSeriesRecordingTask(mContext) + .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete)); + new AsyncDbTask<Void, Void, Void>(mDbExecutor) { @Override protected Void doInBackground(Void... params) { ContentResolver resolver = mContext.getContentResolver(); - String args[] = { inputId }; + String[] args = {inputId}; try { - resolver.delete(RecordedPrograms.CONTENT_URI, - RecordedPrograms.COLUMN_INPUT_ID + " = ?", args); + resolver.delete( + RecordedPrograms.CONTENT_URI, + RecordedPrograms.COLUMN_INPUT_ID + " = ?", + args); } catch (SQLiteException e) { Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e); } @@ -996,7 +1047,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private final Uri mUri; public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) { - super(contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri); + super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri); mUri = uri; } diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index d222003d..63a245a3 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -36,13 +36,12 @@ import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; @@ -51,7 +50,6 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; - import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -60,6 +58,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.Executor; /** * DVR manager class to add and remove recordings. UI can modify recording list through this class, @@ -76,13 +75,15 @@ public class DvrManager { // @GuardedBy("mListener") private final Map<Listener, Handler> mListener = new HashMap<>(); private final Context mAppContext; + private final Executor mDbExecutor; public DvrManager(Context context) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); mAppContext = context.getApplicationContext(); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); - mScheduleManager = appSingletons.getDvrScheduleManager(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mDbExecutor = tvSingletons.getDbExecutor(); + mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager(); + mScheduleManager = tvSingletons.getDvrScheduleManager(); if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms()); } else { @@ -103,37 +104,41 @@ public class DvrManager { }); } if (!mScheduleManager.isInitialized()) { - mScheduleManager.addOnInitializeListener(new OnInitializeListener() { + mScheduleManager.addOnInitializeListener( + new OnInitializeListener() { + @Override + public void onInitialize() { + mScheduleManager.removeOnInitializeListener(this); + if (mDataManager.isInitialized() + && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded( + mDataManager.getRecordedPrograms()); + } + } + }); + } + } + mDataManager.addRecordedProgramListener( + new RecordedProgramListener() { @Override - public void onInitialize() { - mScheduleManager.removeOnInitializeListener(this); - if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { - createSeriesRecordingsForRecordedProgramsIfNeeded( - mDataManager.getRecordedPrograms()); + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) { + return; + } + for (RecordedProgram recordedProgram : recordedPrograms) { + createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); } } - }); - } - } - mDataManager.addRecordedProgramListener(new RecordedProgramListener() { - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) { - return; - } - for (RecordedProgram recordedProgram : recordedPrograms) { - createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); - } - } - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {} - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - // Removing series recording is handled in the SeriesRecordingDetailsFragment. - } - }); + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + // Removing series recording is handled in the + // SeriesRecordingDetailsFragment. + } + }); } private void createSeriesRecordingsForRecordedProgramsIfNeeded( @@ -153,33 +158,38 @@ public class DvrManager { } } - /** - * Schedules a recording for {@code program}. - */ + /** Schedules a recording for {@code program}. */ public ScheduledRecording addSchedule(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return null; } SeriesRecording seriesRecording = getSeriesRecording(program); - return addSchedule(program, seriesRecording == null - ? mScheduleManager.suggestNewPriority() - : seriesRecording.getPriority()); + return addSchedule( + program, + seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : seriesRecording.getPriority()); } /** - * Schedules a recording for {@code program} with the highest priority so that the schedule - * can be recorded. + * Schedules a recording for {@code program} with the highest priority so that the schedule can + * be recorded. */ public ScheduledRecording addScheduleWithHighestPriority(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return null; } SeriesRecording seriesRecording = getSeriesRecording(program); - return addSchedule(program, seriesRecording == null - ? mScheduleManager.suggestNewPriority() - : mScheduleManager.suggestHighestPriority(seriesRecording.getInputId(), - new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()), - seriesRecording.getPriority())); + return addSchedule( + program, + seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : mScheduleManager.suggestHighestPriority( + seriesRecording.getInputId(), + new Range( + program.getStartTimeUtcMillis(), + program.getEndTimeUtcMillis()), + seriesRecording.getPriority())); } private ScheduledRecording addSchedule(Program program, long priority) { @@ -190,21 +200,28 @@ public class DvrManager { } ScheduledRecording schedule; SeriesRecording seriesRecording = getSeriesRecording(program); - schedule = createScheduledRecordingBuilder(input.getId(), program) - .setPriority(priority) - .setSeriesRecordingId(seriesRecording == null ? SeriesRecording.ID_NOT_SET - : seriesRecording.getId()) - .build(); + schedule = + createScheduledRecordingBuilder(input.getId(), program) + .setPriority(priority) + .setSeriesRecordingId( + seriesRecording == null + ? SeriesRecording.ID_NOT_SET + : seriesRecording.getId()) + .build(); mDataManager.addScheduledRecording(schedule); return schedule; } - /** - * Adds a recording schedule with a time range. - */ + /** Adds a recording schedule with a time range. */ public void addSchedule(Channel channel, long startTime, long endTime) { - Log.i(TAG, "Adding scheduled recording of channel " + channel + " starting at " + - Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime)); + Log.i( + TAG, + "Adding scheduled recording of channel " + + channel + + " starting at " + + Utils.toTimeString(startTime) + + " and ending at " + + Utils.toTimeString(endTime)); if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return; } @@ -216,9 +233,7 @@ public class DvrManager { addScheduleInternal(input.getId(), channel.getId(), startTime, endTime); } - /** - * Adds the schedule. - */ + /** Adds the schedule. */ public void addSchedule(ScheduledRecording schedule) { if (mDataManager.isDvrScheduleLoadFinished()) { mDataManager.addScheduledRecording(schedule); @@ -226,19 +241,23 @@ public class DvrManager { } private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) { - mDataManager.addScheduledRecording(ScheduledRecording - .builder(inputId, channelId, startTime, endTime) - .setPriority(mScheduleManager.suggestNewPriority()) - .build()); + mDataManager.addScheduledRecording( + ScheduledRecording.builder(inputId, channelId, startTime, endTime) + .setPriority(mScheduleManager.suggestNewPriority()) + .build()); } - /** - * Adds a new series recording and schedules for the programs with the initial state. - */ - public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) { - Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " - + programsToSchedule); + /** Adds a new series recording and schedules for the programs with the initial state. */ + public SeriesRecording addSeriesRecording( + Program selectedProgram, + List<Program> programsToSchedule, + @SeriesRecording.SeriesState int initialState) { + Log.i( + TAG, + "Adding series recording for program " + + selectedProgram + + ", and schedules: " + + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return null; } @@ -247,10 +266,11 @@ public class DvrManager { Log.e(TAG, "Can't find input for program: " + selectedProgram); return null; } - SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram) - .setPriority(mScheduleManager.suggestNewSeriesPriority()) - .setState(initialState) - .build(); + SeriesRecording seriesRecording = + SeriesRecording.builder(input.getId(), selectedProgram) + .setPriority(mScheduleManager.suggestNewSeriesPriority()) + .setState(initialState) + .build(); mDataManager.addSeriesRecording(seriesRecording); // The schedules for the recorded programs should be added not to create the schedule the // duplicate episodes. @@ -279,9 +299,11 @@ public class DvrManager { // Duplicate schedules can exist, but they will be deleted in a few days. And it's // also guaranteed that the schedules don't belong to any series recordings because // there are no more than one series recordings which have the same program title. - toAdd.add(ScheduledRecording.builder(recordedProgram) - .setPriority(series.getPriority()) - .setSeriesRecordingId(series.getId()).build()); + toAdd.add( + ScheduledRecording.builder(recordedProgram) + .setPriority(series.getPriority()) + .setSeriesRecordingId(series.getId()) + .build()); } } if (!toAdd.isEmpty()) { @@ -291,11 +313,11 @@ public class DvrManager { /** * Adds {@link ScheduledRecording}s for the series recording. - * <p> - * This method doesn't add the series recording. + * + * <p>This method doesn't add the series recording. */ - public void addScheduleToSeriesRecording(SeriesRecording series, - List<Program> programsToSchedule) { + public void addScheduleToSeriesRecording( + SeriesRecording series, List<Program> programsToSchedule) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return; } @@ -311,18 +333,20 @@ public class DvrManager { mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { if (scheduleWithSameProgram.isNotStarted()) { - ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) - .setSeriesRecordingId(series.getId()) - .build(); + ScheduledRecording r = + ScheduledRecording.buildFrom(scheduleWithSameProgram) + .setSeriesRecordingId(series.getId()) + .build(); if (!r.equals(scheduleWithSameProgram)) { toUpdate.add(r); } } } else { - toAdd.add(createScheduledRecordingBuilder(input.getId(), program) - .setPriority(series.getPriority()) - .setSeriesRecordingId(series.getId()) - .build()); + toAdd.add( + createScheduledRecordingBuilder(input.getId(), program) + .setPriority(series.getPriority()) + .setSeriesRecordingId(series.getId()) + .build()); } } if (!toAdd.isEmpty()) { @@ -333,9 +357,7 @@ public class DvrManager { } } - /** - * Updates the series recording. - */ + /** Updates the series recording. */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); @@ -344,7 +366,7 @@ public class DvrManager { // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment. if (previousSeries.getChannelOption() != series.getChannelOption() || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE - && previousSeries.getChannelId() != series.getChannelId())) { + && previousSeries.getChannelId() != series.getChannelId())) { List<ScheduledRecording> schedules = mDataManager.getScheduledRecordings(series.getId()); List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); @@ -365,20 +387,21 @@ public class DvrManager { schedulesToRemove.add(deletedSchedule); } } - mDataManager.removeScheduledRecording(true, - ScheduledRecording.toArray(schedulesToRemove)); + mDataManager.removeScheduledRecording( + true, ScheduledRecording.toArray(schedulesToRemove)); } } mDataManager.updateSeriesRecording(series); - if (previousSeries == null - || previousSeries.getPriority() != series.getPriority()) { + if (previousSeries == null || previousSeries.getPriority() != series.getPriority()) { long priority = series.getPriority(); List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); - for (ScheduledRecording schedule - : mDataManager.getScheduledRecordings(series.getId())) { + for (ScheduledRecording schedule : + mDataManager.getScheduledRecordings(series.getId())) { if (schedule.isNotStarted() || schedule.isInProgress()) { - schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) - .setPriority(priority).build()); + schedulesToUpdate.add( + ScheduledRecording.buildFrom(schedule) + .setPriority(priority) + .build()); } } if (!schedulesToUpdate.isEmpty()) { @@ -411,28 +434,26 @@ public class DvrManager { mDataManager.removeSeriesRecording(series); } - /** - * Stops the currently recorded program - */ + /** Stops the currently recorded program */ public void stopRecording(final ScheduledRecording recording) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return; } synchronized (mListener) { for (final Entry<Listener, Handler> entry : mListener.entrySet()) { - entry.getValue().post(new Runnable() { - @Override - public void run() { - entry.getKey().onStopRecordingRequested(recording); - } - }); + entry.getValue() + .post( + new Runnable() { + @Override + public void run() { + entry.getKey().onStopRecordingRequested(recording); + } + }); } } } - /** - * Removes scheduled recordings or an existing recordings. - */ + /** Removes scheduled recordings or an existing recordings. */ public void removeScheduledRecording(ScheduledRecording... schedules) { Log.i(TAG, "Removing " + Arrays.asList(schedules)); if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { @@ -447,9 +468,7 @@ public class DvrManager { } } - /** - * Removes scheduled recordings without changing to the DELETED state. - */ + /** Removes scheduled recordings without changing to the DELETED state. */ public void forceRemoveScheduledRecording(ScheduledRecording... schedules) { Log.i(TAG, "Force removing " + Arrays.asList(schedules)); if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { @@ -464,9 +483,7 @@ public class DvrManager { } } - /** - * Removes the recorded program. It deletes the file if possible. - */ + /** Removes the recorded program. It deletes the file if possible. */ public void removeRecordedProgram(Uri recordedProgramUri) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; @@ -474,9 +491,7 @@ public class DvrManager { removeRecordedProgram(ContentUris.parseId(recordedProgramUri)); } - /** - * Removes the recorded program. It deletes the file if possible. - */ + /** Removes the recorded program. It deletes the file if possible. */ public void removeRecordedProgram(long recordedProgramId) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; @@ -487,14 +502,12 @@ public class DvrManager { } } - /** - * Removes the recorded program. It deletes the file if possible. - */ + /** Removes the recorded program. It deletes the file if possible. */ public void removeRecordedProgram(final RecordedProgram recordedProgram) { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; } - new AsyncDbTask<Void, Void, Integer>() { + new AsyncDbTask<Void, Void, Integer>(mDbExecutor) { @Override protected Integer doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); @@ -526,7 +539,7 @@ public class DvrManager { dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build()); } } - new AsyncDbTask<Void, Void, Boolean>() { + new AsyncDbTask<Void, Void, Boolean>(mDbExecutor) { @Override protected Boolean doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); @@ -556,9 +569,7 @@ public class DvrManager { }.executeOnDbThread(); } - /** - * Updates the scheduled recording. - */ + /** Updates the scheduled recording. */ public void updateScheduledRecording(ScheduledRecording recording) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { mDataManager.updateScheduledRecording(recording); @@ -566,8 +577,8 @@ public class DvrManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this program is. + * Returns priority ordered list of all scheduled recordings that will not be recorded if this + * program is. * * @see DvrScheduleManager#getConflictingSchedules(Program) */ @@ -579,13 +590,13 @@ public class DvrManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this channel is. + * Returns priority ordered list of all scheduled recordings that will not be recorded if this + * channel is. * * @see DvrScheduleManager#getConflictingSchedules(long, long, long) */ - public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs, - long endTimeMs) { + public List<ScheduledRecording> getConflictingSchedules( + long channelId, long startTimeMs, long endTimeMs) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return Collections.emptyList(); } @@ -595,8 +606,8 @@ public class DvrManager { /** * Checks if the schedule is conflicting. * - * <p>Note that the {@code schedule} should be the existing one. If not, this returns - * {@code false}. + * <p>Note that the {@code schedule} should be the existing one. If not, this returns {@code + * false}. */ public boolean isConflicting(ScheduledRecording schedule) { return schedule != null @@ -605,8 +616,8 @@ public class DvrManager { } /** - * Returns priority ordered list of all scheduled recording that will not be recorded if - * this channel is tuned to. + * Returns priority ordered list of all scheduled recording that will not be recorded if this + * channel is tuned to. * * @see DvrScheduleManager#getConflictingSchedulesForTune */ @@ -617,22 +628,18 @@ public class DvrManager { return mScheduleManager.getConflictingSchedulesForTune(channelId); } - /** - * Sets the highest priority to the schedule. - */ + /** Sets the highest priority to the schedule. */ public void setHighestPriority(ScheduledRecording schedule) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { long newPriority = mScheduleManager.suggestHighestPriority(schedule); if (newPriority != schedule.getPriority()) { - mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) - .setPriority(newPriority).build()); + mDataManager.updateScheduledRecording( + ScheduledRecording.buildFrom(schedule).setPriority(newPriority).build()); } } } - /** - * Suggests the higher priority than the schedules which overlap with {@code schedule}. - */ + /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */ public long suggestHighestPriority(ScheduledRecording schedule) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { return mScheduleManager.suggestHighestPriority(schedule); @@ -642,9 +649,9 @@ public class DvrManager { /** * Returns {@code true} if the channel can be recorded. - * <p> - * Note that this method doesn't check the conflict of the schedule or available tuners. - * This can be called from the UI before the schedules are loaded. + * + * <p>Note that this method doesn't check the conflict of the schedule or available tuners. This + * can be called from the UI before the schedules are loaded. */ public boolean isChannelRecordable(Channel channel) { if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) { @@ -661,23 +668,27 @@ public class DvrManager { if (!info.canRecord()) { return false; } - Program program = TvApplication.getSingletons(mAppContext).getProgramDataManager() - .getCurrentProgram(channel.getId()); + Program program = + TvSingletons.getSingletons(mAppContext) + .getProgramDataManager() + .getCurrentProgram(channel.getId()); return program == null || !program.isRecordingProhibited(); } /** * Returns {@code true} if the program can be recorded. - * <p> - * Note that this method doesn't check the conflict of the schedule or available tuners. - * This can be called from the UI before the schedules are loaded. + * + * <p>Note that this method doesn't check the conflict of the schedule or available tuners. This + * can be called from the UI before the schedules are loaded. */ public boolean isProgramRecordable(Program program) { if (!mDataManager.isInitialized()) { return false; } - Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager() - .getChannel(program.getChannelId()); + Channel channel = + TvSingletons.getSingletons(mAppContext) + .getChannelDataManager() + .getChannel(program.getChannelId()); if (channel == null || channel.isRecordingProhibited()) { return false; } @@ -691,8 +702,8 @@ public class DvrManager { /** * Returns the current recording for the channel. - * <p> - * This can be called from the UI before the schedules are loaded. + * + * <p>This can be called from the UI before the schedules are loaded. */ public ScheduledRecording getCurrentRecording(long channelId) { if (!mDataManager.isDvrScheduleLoadFinished()) { @@ -707,8 +718,8 @@ public class DvrManager { } /** - * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to - * the series recording {@code seriesRecordingId}. + * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to the + * series recording {@code seriesRecordingId}. */ public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) { if (!mDataManager.isDvrScheduleLoadFinished()) { @@ -723,9 +734,7 @@ public class DvrManager { return schedules; } - /** - * Returns the series recording related to the program. - */ + /** Returns the series recording related to the program. */ @Nullable public SeriesRecording getSeriesRecording(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { @@ -735,8 +744,8 @@ public class DvrManager { } /** - * Returns if there are valid items. Valid item contains {@link RecordedProgram}, - * available {@link ScheduledRecording} and {@link SeriesRecording}. + * Returns if there are valid items. Valid item contains {@link RecordedProgram}, available + * {@link ScheduledRecording} and {@link SeriesRecording}. */ public boolean hasValidItems() { return !(mDataManager.getRecordedPrograms().isEmpty() @@ -768,8 +777,8 @@ public class DvrManager { * Returns ScheduledRecording.builder based on {@code program}. If program is already started, * recording started time is clipped to the current time. */ - private ScheduledRecording.Builder createScheduledRecordingBuilder(String inputId, - Program program) { + private ScheduledRecording.Builder createScheduledRecordingBuilder( + String inputId, Program program) { ScheduledRecording.Builder builder = ScheduledRecording.builder(inputId, program); long time = System.currentTimeMillis(); if (program.getStartTimeUtcMillis() < time && time < program.getEndTimeUtcMillis()) { @@ -778,13 +787,13 @@ public class DvrManager { return builder; } - /** - * Returns a schedule which matches to the given episode. - */ - public ScheduledRecording getScheduledRecording(String title, String seasonNumber, - String episodeNumber) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null - || seasonNumber == null || episodeNumber == null) { + /** Returns a schedule which matches to the given episode. */ + public ScheduledRecording getScheduledRecording( + String title, String seasonNumber, String episodeNumber) { + if (!SoftPreconditions.checkState(mDataManager.isInitialized()) + || title == null + || seasonNumber == null + || episodeNumber == null) { return null; } for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { @@ -797,13 +806,13 @@ public class DvrManager { return null; } - /** - * Returns a recorded program which is the same episode as the given {@code program}. - */ - public RecordedProgram getRecordedProgram(String title, String seasonNumber, - String episodeNumber) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null - || seasonNumber == null || episodeNumber == null) { + /** Returns a recorded program which is the same episode as the given {@code program}. */ + public RecordedProgram getRecordedProgram( + String title, String seasonNumber, String episodeNumber) { + if (!SoftPreconditions.checkState(mDataManager.isInitialized()) + || title == null + || seasonNumber == null + || episodeNumber == null) { return null; } for (RecordedProgram r : mDataManager.getRecordedPrograms()) { @@ -820,13 +829,14 @@ public class DvrManager { @WorkerThread private void removeRecordedData(Uri dataUri) { try { - if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) + if (dataUri != null + && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) && dataUri.getPath() != null) { File recordedProgramPath = new File(dataUri.getPath()); if (!recordedProgramPath.exists()) { if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath); } else { - Utils.deleteDirOrFile(recordedProgramPath); + CommonUtils.deleteDirOrFile(recordedProgramPath); if (DEBUG) { Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri); } @@ -834,16 +844,19 @@ public class DvrManager { } } catch (SecurityException e) { if (DEBUG) { - Log.d(TAG, "To delete this recorded program, please manually delete video data at" - + "\nadb shell rm -rf " + dataUri); + Log.d( + TAG, + "To delete this recorded program, please manually delete video data at" + + "\nadb shell rm -rf " + + dataUri); } } } /** * Remove all the records related to the input. - * <p> - * Note that this should be called after the input was removed. + * + * <p>Note that this should be called after the input was removed. */ public void forgetStorage(String inputId) { if (mDataManager.isInitialized()) { diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index b72117aa..d5126b12 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -25,21 +25,18 @@ import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; import android.util.Range; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.recorder.InputTaskScheduler; -import com.android.tv.util.CompositeComparator; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.recorder.InputTaskScheduler; +import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -50,21 +47,16 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -/** - * A class to manage the schedules. - */ +/** A class to manage the schedules. */ @TargetApi(Build.VERSION_CODES.N) @MainThread +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public class DvrScheduleManager { private static final String TAG = "DvrScheduleManager"; - /** - * The default priority of scheduled recording. - */ + /** The default priority of scheduled recording. */ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; - /** - * The default priority of series recording. - */ + /** The default priority of series recording. */ public static final long DEFAULT_SERIES_PRIORITY = DEFAULT_PRIORITY >> 1; // The new priority will have the offset from the existing one. private static final long PRIORITY_OFFSET = 1024; @@ -102,9 +94,9 @@ public class DvrScheduleManager { public DvrScheduleManager(Context context) { mContext = context; - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mDataManager = (DvrDataManagerImpl) appSingletons.getDvrDataManager(); - mChannelDataManager = appSingletons.getChannelDataManager(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mDataManager = (DvrDataManagerImpl) tvSingletons.getDvrDataManager(); + mChannelDataManager = tvSingletons.getChannelDataManager(); if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { buildData(); } else { @@ -119,146 +111,151 @@ public class DvrScheduleManager { } }); } - ScheduledRecordingListener scheduledRecordingListener = new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - if (!mInitialized) { - return; - } - for (ScheduledRecording schedule : scheduledRecordings) { - if (!schedule.isNotStarted() && !schedule.isInProgress()) { - continue; - } - TvInputInfo input = Utils - .getTvInputInfoForInputId(mContext, schedule.getInputId()); - if (!SoftPreconditions.checkArgument(input != null, TAG, - "Input was removed for : " + schedule)) { - // Input removed. - mInputScheduleMap.remove(schedule.getInputId()); - mInputConflictInfoMap.remove(schedule.getInputId()); - continue; - } - String inputId = input.getId(); - List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); - if (schedules == null) { - schedules = new ArrayList<>(); - mInputScheduleMap.put(inputId, schedules); + ScheduledRecordingListener scheduledRecordingListener = + new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded( + ScheduledRecording... scheduledRecordings) { + if (!mInitialized) { + return; + } + for (ScheduledRecording schedule : scheduledRecordings) { + if (!schedule.isNotStarted() && !schedule.isInProgress()) { + continue; + } + TvInputInfo input = + Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument( + input != null, TAG, "Input was removed for : %s", schedule)) { + // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); + continue; + } + String inputId = input.getId(); + List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); + if (schedules == null) { + schedules = new ArrayList<>(); + mInputScheduleMap.put(inputId, schedules); + } + schedules.add(schedule); + } + onSchedulesChanged(); + notifyScheduledRecordingAdded(scheduledRecordings); } - schedules.add(schedule); - } - onSchedulesChanged(); - notifyScheduledRecordingAdded(scheduledRecordings); - } - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - if (!mInitialized) { - return; - } - for (ScheduledRecording schedule : scheduledRecordings) { - TvInputInfo input = Utils - .getTvInputInfoForInputId(mContext, schedule.getInputId()); - if (input == null) { - // Input removed. - mInputScheduleMap.remove(schedule.getInputId()); - mInputConflictInfoMap.remove(schedule.getInputId()); - continue; - } - String inputId = input.getId(); - List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); - if (schedules != null) { - schedules.remove(schedule); - if (schedules.isEmpty()) { - mInputScheduleMap.remove(inputId); + @Override + public void onScheduledRecordingRemoved( + ScheduledRecording... scheduledRecordings) { + if (!mInitialized) { + return; } - } - Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); - if (conflictInfo != null) { - conflictInfo.remove(schedule.getId()); - if (conflictInfo.isEmpty()) { - mInputConflictInfoMap.remove(inputId); + for (ScheduledRecording schedule : scheduledRecordings) { + TvInputInfo input = + Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (input == null) { + // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); + continue; + } + String inputId = input.getId(); + List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); + if (schedules != null) { + schedules.remove(schedule); + if (schedules.isEmpty()) { + mInputScheduleMap.remove(inputId); + } + } + Map<Long, ConflictInfo> conflictInfo = + mInputConflictInfoMap.get(inputId); + if (conflictInfo != null) { + conflictInfo.remove(schedule.getId()); + if (conflictInfo.isEmpty()) { + mInputConflictInfoMap.remove(inputId); + } + } } + onSchedulesChanged(); + notifyScheduledRecordingRemoved(scheduledRecordings); } - } - onSchedulesChanged(); - notifyScheduledRecordingRemoved(scheduledRecordings); - } - @Override - public void onScheduledRecordingStatusChanged( - ScheduledRecording... scheduledRecordings) { - if (!mInitialized) { - return; - } - for (ScheduledRecording schedule : scheduledRecordings) { - TvInputInfo input = Utils - .getTvInputInfoForInputId(mContext, schedule.getInputId()); - if (!SoftPreconditions.checkArgument(input != null, TAG, - "Input was removed for : " + schedule)) { - // Input removed. - mInputScheduleMap.remove(schedule.getInputId()); - mInputConflictInfoMap.remove(schedule.getInputId()); - continue; - } - String inputId = input.getId(); - List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); - if (schedules == null) { - schedules = new ArrayList<>(); - mInputScheduleMap.put(inputId, schedules); - } - // Compare ID because ScheduledRecording.equals() doesn't work if the state - // is changed. - for (Iterator<ScheduledRecording> i = schedules.iterator(); i.hasNext(); ) { - if (i.next().getId() == schedule.getId()) { - i.remove(); - break; + @Override + public void onScheduledRecordingStatusChanged( + ScheduledRecording... scheduledRecordings) { + if (!mInitialized) { + return; } - } - if (schedule.isNotStarted() || schedule.isInProgress()) { - schedules.add(schedule); - } - if (schedules.isEmpty()) { - mInputScheduleMap.remove(inputId); - } - // Update conflict list as well - Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); - if (conflictInfo != null) { - ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); - if (oldConflictInfo != null) { - oldConflictInfo.schedule = schedule; + for (ScheduledRecording schedule : scheduledRecordings) { + TvInputInfo input = + Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument( + input != null, TAG, "Input was removed for : %s", schedule)) { + // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); + continue; + } + String inputId = input.getId(); + List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); + if (schedules == null) { + schedules = new ArrayList<>(); + mInputScheduleMap.put(inputId, schedules); + } + // Compare ID because ScheduledRecording.equals() doesn't work if the + // state + // is changed. + for (Iterator<ScheduledRecording> i = schedules.iterator(); + i.hasNext(); ) { + if (i.next().getId() == schedule.getId()) { + i.remove(); + break; + } + } + if (schedule.isNotStarted() || schedule.isInProgress()) { + schedules.add(schedule); + } + if (schedules.isEmpty()) { + mInputScheduleMap.remove(inputId); + } + // Update conflict list as well + Map<Long, ConflictInfo> conflictInfo = + mInputConflictInfoMap.get(inputId); + if (conflictInfo != null) { + ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); + if (oldConflictInfo != null) { + oldConflictInfo.schedule = schedule; + } + } } + onSchedulesChanged(); + notifyScheduledRecordingStatusChanged(scheduledRecordings); } - } - onSchedulesChanged(); - notifyScheduledRecordingStatusChanged(scheduledRecordings); - } - }; + }; mDataManager.addScheduledRecordingListener(scheduledRecordingListener); - ChannelDataManager.Listener channelDataManagerListener = new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - if (mDataManager.isDvrScheduleLoadFinished() && !mInitialized) { - buildData(); - } - } + ChannelDataManager.Listener channelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + if (mDataManager.isDvrScheduleLoadFinished() && !mInitialized) { + buildData(); + } + } - @Override - public void onChannelListUpdated() { - if (mDataManager.isDvrScheduleLoadFinished()) { - buildData(); - } - } + @Override + public void onChannelListUpdated() { + if (mDataManager.isDvrScheduleLoadFinished()) { + buildData(); + } + } - @Override - public void onChannelBrowsableChanged() { - } - }; + @Override + public void onChannelBrowsableChanged() {} + }; mChannelDataManager.addListener(channelDataManagerListener); } - /** - * Returns the started recordings for the given input. - */ + /** Returns the started recordings for the given input. */ private List<ScheduledRecording> getStartedRecordings(String inputId) { if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { return Collections.emptyList(); @@ -337,39 +334,29 @@ public class DvrScheduleManager { } } - /** - * Returns {@code true} if this class has been initialized. - */ + /** Returns {@code true} if this class has been initialized. */ public boolean isInitialized() { return mInitialized; } - /** - * Adds a {@link ScheduledRecordingListener}. - */ + /** Adds a {@link ScheduledRecordingListener}. */ public final void addScheduledRecordingListener(ScheduledRecordingListener listener) { mScheduledRecordingListeners.add(listener); } - /** - * Removes a {@link ScheduledRecordingListener}. - */ + /** Removes a {@link ScheduledRecordingListener}. */ public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) { mScheduledRecordingListeners.remove(listener); } - /** - * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. - */ + /** Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener. */ private void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { l.onScheduledRecordingAdded(scheduledRecordings); } } - /** - * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener. - */ + /** Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener. */ private void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { l.onScheduledRecordingRemoved(scheduledRecordings); @@ -385,48 +372,36 @@ public class DvrScheduleManager { } } - /** - * Adds a {@link OnInitializeListener}. - */ + /** Adds a {@link OnInitializeListener}. */ public final void addOnInitializeListener(OnInitializeListener listener) { mOnInitializeListeners.add(listener); } - /** - * Removes a {@link OnInitializeListener}. - */ + /** Removes a {@link OnInitializeListener}. */ public final void removeOnInitializeListener(OnInitializeListener listener) { mOnInitializeListeners.remove(listener); } - /** - * Calls {@link OnInitializeListener#onInitialize} for each listener. - */ + /** Calls {@link OnInitializeListener#onInitialize} for each listener. */ private void notifyInitialize() { for (OnInitializeListener l : mOnInitializeListeners) { l.onInitialize(); } } - /** - * Adds a {@link OnConflictStateChangeListener}. - */ + /** Adds a {@link OnConflictStateChangeListener}. */ public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) { mOnConflictStateChangeListeners.add(listener); } - /** - * Removes a {@link OnConflictStateChangeListener}. - */ + /** Removes a {@link OnConflictStateChangeListener}. */ public final void removeOnConflictStateChangeListener(OnConflictStateChangeListener listener) { mOnConflictStateChangeListeners.remove(listener); } - /** - * Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener. - */ - private void notifyConflictStateChange(boolean conflict, - ScheduledRecording... scheduledRecordings) { + /** Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener. */ + private void notifyConflictStateChange( + boolean conflict, ScheduledRecording... scheduledRecordings) { for (OnConflictStateChangeListener l : mOnConflictStateChangeListeners) { l.onConflictStateChange(conflict, scheduledRecordings); } @@ -434,8 +409,8 @@ public class DvrScheduleManager { /** * Returns the priority for the program if it is recorded. - * <p> - * The recording will have the higher priority than the existing ones. + * + * <p>The recording will have the higher priority than the existing ones. */ public long suggestNewPriority() { if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { @@ -454,9 +429,7 @@ public class DvrScheduleManager { return highestPriority + PRIORITY_OFFSET; } - /** - * Suggests the higher priority than the schedules which overlap with {@code schedule}. - */ + /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */ public long suggestHighestPriority(ScheduledRecording schedule) { List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId()); if (schedules == null) { @@ -464,7 +437,8 @@ public class DvrScheduleManager { } long highestPriority = Long.MIN_VALUE; for (ScheduledRecording r : schedules) { - if (!r.equals(schedule) && r.isOverLapping(schedule) + if (!r.equals(schedule) + && r.isOverLapping(schedule) && r.getPriority() > highestPriority) { highestPriority = r.getPriority(); } @@ -475,9 +449,7 @@ public class DvrScheduleManager { return highestPriority + PRIORITY_OFFSET; } - /** - * Suggests the higher priority than the schedules which overlap with {@code schedule}. - */ + /** Suggests the higher priority than the schedules which overlap with {@code schedule}. */ public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) { List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); if (schedules == null) { @@ -497,8 +469,8 @@ public class DvrScheduleManager { /** * Returns the priority for a series recording. - * <p> - * The recording will have the higher priority than the existing series. + * + * <p>The recording will have the higher priority than the existing series. */ public long suggestNewSeriesPriority() { if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) { @@ -510,7 +482,7 @@ public class DvrScheduleManager { /** * Returns the priority for a series recording by order of series recording priority. * - * Higher order will have higher priority. + * <p>Higher order will have higher priority. */ public static long suggestSeriesPriority(int order) { return DEFAULT_SERIES_PRIORITY + order * PRIORITY_OFFSET; @@ -527,21 +499,23 @@ public class DvrScheduleManager { } /** - * Returns a sorted list of all scheduled recordings that will not be recorded if - * this program is going to be recorded, with their priorities in decending order. - * <p> - * An empty list means there is no conflicts. If there is conflict, a priority higher than + * Returns a sorted list of all scheduled recordings that will not be recorded if this program + * is going to be recorded, with their priorities in decending order. + * + * <p>An empty list means there is no conflicts. If there is conflict, a priority higher than * the first recording in the returned list should be assigned to the new schedule of this * program to guarantee the program would be completely recorded. */ public List<ScheduledRecording> getConflictingSchedules(Program program) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); - SoftPreconditions.checkState(Program.isValid(program), TAG, - "Program is invalid: " + program); SoftPreconditions.checkState( - program.getStartTimeUtcMillis() < program.getEndTimeUtcMillis(), TAG, + Program.isProgramValid(program), TAG, "Program is invalid: " + program); + SoftPreconditions.checkState( + program.getStartTimeUtcMillis() < program.getEndTimeUtcMillis(), + TAG, "Program duration is empty: " + program); - if (!mInitialized || !Program.isValid(program) + if (!mInitialized + || !Program.isProgramValid(program) || program.getStartTimeUtcMillis() >= program.getEndTimeUtcMillis()) { return Collections.emptyList(); } @@ -549,17 +523,19 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - return getConflictingSchedules(input, Collections.singletonList( - ScheduledRecording.builder(input.getId(), program) - .setPriority(suggestHighestPriority()) - .build())); + return getConflictingSchedules( + input, + Collections.singletonList( + ScheduledRecording.builder(input.getId(), program) + .setPriority(suggestHighestPriority()) + .build())); } /** * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording} * recording. - * <p> - * Any empty list means there is no conflicts. + * + * <p>Any empty list means there is no conflicts. */ public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); @@ -571,8 +547,8 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings( - seriesRecording.getId()); + List<ScheduledRecording> scheduledRecordingForSeries = + mDataManager.getScheduledRecordings(seriesRecording.getId()); List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>(); for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) { if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) { @@ -586,15 +562,15 @@ public class DvrScheduleManager { } /** - * Returns a sorted list of all scheduled recordings that will not be recorded if - * this channel is going to be recorded, with their priority in decending order. - * <p> - * An empty list means there is no conflicts. If there is conflict, a priority higher than + * Returns a sorted list of all scheduled recordings that will not be recorded if this channel + * is going to be recorded, with their priority in decending order. + * + * <p>An empty list means there is no conflicts. If there is conflict, a priority higher than * the first recording in the returned list should be assigned to the new schedule of this * channel to guarantee the channel would be completely recorded in the designated time range. */ - public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs, - long endTimeMs) { + public List<ScheduledRecording> getConflictingSchedules( + long channelId, long startTimeMs, long endTimeMs) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); SoftPreconditions.checkState(startTimeMs < endTimeMs, TAG, "Recording duration is empty."); @@ -605,10 +581,12 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - return getConflictingSchedules(input, Collections.singletonList( - ScheduledRecording.builder(input.getId(), channelId, startTimeMs, endTimeMs) - .setPriority(suggestHighestPriority()) - .build())); + return getConflictingSchedules( + input, + Collections.singletonList( + ScheduledRecording.builder(input.getId(), channelId, startTimeMs, endTimeMs) + .setPriority(suggestHighestPriority()) + .build())); } /** @@ -633,14 +611,14 @@ public class DvrScheduleManager { /** * Checks if the schedule is conflicting. * - * <p>Note that the {@code schedule} should be the existing one. If not, this returns - * {@code false}. + * <p>Note that the {@code schedule} should be the existing one. If not, this returns {@code + * false}. */ public boolean isConflicting(ScheduledRecording schedule) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); - SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " - + schedule.getChannelId()); + SoftPreconditions.checkState( + input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId()); if (!mInitialized || input == null) { return false; } @@ -651,15 +629,15 @@ public class DvrScheduleManager { /** * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be * recorded even if the priority of the schedule is not raised. - * <p> - * If the given schedule is not conflicting or is totally conflicting, i.e., cannot be recorded - * at all, this method returns {@code false} in both cases. + * + * <p>If the given schedule is not conflicting or is totally conflicting, i.e., cannot be + * recorded at all, this method returns {@code false} in both cases. */ public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); - SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " - + schedule.getChannelId()); + SoftPreconditions.checkState( + input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId()); if (!mInitialized || input == null) { return false; } @@ -672,27 +650,35 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this channel is tuned to. + * Returns priority ordered list of all scheduled recordings that will not be recorded if this + * channel is tuned to. */ public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId); - SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: " - + channelId); + SoftPreconditions.checkState( + input != null, TAG, "Can't find input for channel ID: " + channelId); if (!mInitialized || channelId == Channel.INVALID_ID || input == null) { return Collections.emptyList(); } - return getConflictingSchedulesForTune(input.getId(), channelId, System.currentTimeMillis(), - suggestHighestPriority(), getStartedRecordings(input.getId()), + return getConflictingSchedulesForTune( + input.getId(), + channelId, + System.currentTimeMillis(), + suggestHighestPriority(), + getStartedRecordings(input.getId()), input.getTunerCount()); } @VisibleForTesting - public static List<ScheduledRecording> getConflictingSchedulesForTune(String inputId, - long channelId, long currentTimeMs, long newPriority, - List<ScheduledRecording> startedRecordings, int tunerCount) { + public static List<ScheduledRecording> getConflictingSchedulesForTune( + String inputId, + long channelId, + long currentTimeMs, + long newPriority, + List<ScheduledRecording> startedRecordings, + int tunerCount) { boolean channelFound = false; for (ScheduledRecording schedule : startedRecordings) { if (schedule.getChannelId() == channelId) { @@ -704,10 +690,10 @@ public class DvrScheduleManager { if (!channelFound) { // The current channel is not being recorded. schedules = new ArrayList<>(startedRecordings); - schedules.add(ScheduledRecording - .builder(inputId, channelId, currentTimeMs, currentTimeMs + 1) - .setPriority(newPriority) - .build()); + schedules.add( + ScheduledRecording.builder(inputId, channelId, currentTimeMs, currentTimeMs + 1) + .setPriority(newPriority) + .build()); } else { schedules = startedRecordings; } @@ -715,17 +701,17 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * the user keeps watching this channel. - * <p> - * Note that if the user keeps watching the channel, the channel can be recorded. + * Returns priority ordered list of all scheduled recordings that will not be recorded if the + * user keeps watching this channel. + * + * <p>Note that if the user keeps watching the channel, the channel can be recorded. */ public List<ScheduledRecording> getConflictingSchedulesForWatching(long channelId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID"); TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId); - SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: " - + channelId); + SoftPreconditions.checkState( + input != null, TAG, "Can't find input for channel ID: " + channelId); if (!mInitialized || channelId == Channel.INVALID_ID || input == null) { return Collections.emptyList(); } @@ -733,12 +719,17 @@ public class DvrScheduleManager { if (schedules == null || schedules.isEmpty()) { return Collections.emptyList(); } - return getConflictingSchedulesForWatching(input.getId(), channelId, - System.currentTimeMillis(), suggestNewPriority(), schedules, input.getTunerCount()); + return getConflictingSchedulesForWatching( + input.getId(), + channelId, + System.currentTimeMillis(), + suggestNewPriority(), + schedules, + input.getTunerCount()); } - private List<ScheduledRecording> getConflictingSchedules(TvInputInfo input, - List<ScheduledRecording> schedulesToAdd) { + private List<ScheduledRecording> getConflictingSchedules( + TvInputInfo input, List<ScheduledRecording> schedulesToAdd) { SoftPreconditions.checkNotNull(input); if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); @@ -751,9 +742,13 @@ public class DvrScheduleManager { } @VisibleForTesting - static List<ScheduledRecording> getConflictingSchedulesForWatching(String inputId, - long channelId, long currentTimeMs, long newPriority, - @NonNull List<ScheduledRecording> schedules, int tunerCount) { + static List<ScheduledRecording> getConflictingSchedulesForWatching( + String inputId, + long channelId, + long currentTimeMs, + long newPriority, + @NonNull List<ScheduledRecording> schedules, + int tunerCount) { List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); List<ScheduledRecording> schedulesSameChannel = new ArrayList<>(); for (ScheduledRecording schedule : schedules) { @@ -763,10 +758,10 @@ public class DvrScheduleManager { } } // Assume that the user will watch the current channel forever. - schedulesToCheck.add(ScheduledRecording - .builder(inputId, channelId, currentTimeMs, Long.MAX_VALUE) - .setPriority(newPriority) - .build()); + schedulesToCheck.add( + ScheduledRecording.builder(inputId, channelId, currentTimeMs, Long.MAX_VALUE) + .setPriority(newPriority) + .build()); List<ScheduledRecording> result = new ArrayList<>(); result.addAll(getConflictingSchedules(schedulesSameChannel, 1)); result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount)); @@ -775,8 +770,10 @@ public class DvrScheduleManager { } @VisibleForTesting - static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedulesToAdd, - List<ScheduledRecording> currentSchedules, int tunerCount) { + static List<ScheduledRecording> getConflictingSchedules( + List<ScheduledRecording> schedulesToAdd, + List<ScheduledRecording> currentSchedules, + int tunerCount) { List<ScheduledRecording> schedulesToCheck = new ArrayList<>(currentSchedules); // When the duplicate schedule is to be added, remove the current duplicate recording. for (Iterator<ScheduledRecording> iter = schedulesToCheck.iterator(); iter.hasNext(); ) { @@ -805,9 +802,7 @@ public class DvrScheduleManager { return getConflictingSchedules(schedulesToCheck, tunerCount, ranges); } - /** - * Returns all conflicting scheduled recordings for the given schedules and count of tuner. - */ + /** Returns all conflicting scheduled recordings for the given schedules and count of tuner. */ public static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount) { return getConflictingSchedules(schedules, tunerCount, null); @@ -825,21 +820,21 @@ public class DvrScheduleManager { } @VisibleForTesting - static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules, - int tunerCount) { + static List<ConflictInfo> getConflictingSchedulesInfo( + List<ScheduledRecording> schedules, int tunerCount) { return getConflictingSchedulesInfo(schedules, tunerCount, null); } /** * This is the core method to calculate all the conflicting schedules (in given periods). - * <p> - * Note that this method will ignore duplicated schedules with a same hash code. (Please refer - * to {@link ScheduledRecording#hashCode}.) + * + * <p>Note that this method will ignore duplicated schedules with a same hash code. (Please + * refer to {@link ScheduledRecording#hashCode}.) * * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean - * value denotes if the scheduled recording is partially conflicting, i.e., is possible - * to be partially recorded under the given schedules and tuner count {@code true}, - * or not {@code false}. + * value denotes if the scheduled recording is partially conflicting, i.e., is possible to + * be partially recorded under the given schedules and tuner count {@code true}, or not + * {@code false}. */ private static List<ConflictInfo> getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { @@ -886,14 +881,19 @@ public class DvrScheduleManager { if (earliestEndTime < schedule.getEndTimeMs()) { // The schedule can starts when other recording ends even though it's // clipped. - ScheduledRecording modifiedSchedule = ScheduledRecording.buildFrom(schedule) - .setStartTimeMs(earliestEndTime).build(); + ScheduledRecording modifiedSchedule = + ScheduledRecording.buildFrom(schedule) + .setStartTimeMs(earliestEndTime) + .build(); ScheduledRecording originalSchedule = modified2OriginalSchedules.getOrDefault(schedule, schedule); modified2OriginalSchedules.put(modifiedSchedule, originalSchedule); - int insertPosition = Collections.binarySearch(schedulesToCheck, - modifiedSchedule, - ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + int insertPosition = + Collections.binarySearch( + schedulesToCheck, + modifiedSchedule, + ScheduledRecording + .START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); if (insertPosition >= 0) { schedulesToCheck.add(insertPosition, modifiedSchedule); } else { @@ -921,17 +921,19 @@ public class DvrScheduleManager { } } List<ConflictInfo> result = new ArrayList<>(conflicts.values()); - Collections.sort(result, new Comparator<ConflictInfo>() { - @Override - public int compare(ConflictInfo lhs, ConflictInfo rhs) { - return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); - } - }); + Collections.sort( + result, + new Comparator<ConflictInfo>() { + @Override + public int compare(ConflictInfo lhs, ConflictInfo rhs) { + return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); + } + }); return result; } - private static void removeFinishedRecordings(List<ScheduledRecording> recordings, - long currentTimeMs) { + private static void removeFinishedRecordings( + List<ScheduledRecording> recordings, long currentTimeMs) { for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) { if (iter.next().getEndTimeMs() <= currentTimeMs) { iter.remove(); @@ -939,11 +941,9 @@ public class DvrScheduleManager { } } - /** - * @see InputTaskScheduler#getReplacableTask - */ - private static ScheduledRecording findReplaceableRecording(List<ScheduledRecording> recordings, - ScheduledRecording schedule) { + /** @see InputTaskScheduler#getReplacableTask */ + private static ScheduledRecording findReplaceableRecording( + List<ScheduledRecording> recordings, ScheduledRecording schedule) { // Returns the recording with the following priority. // 1. The recording with the lowest priority is returned. // 2. If the priorities are the same, the recording which finishes early is returned. @@ -980,30 +980,24 @@ public class DvrScheduleManager { } } - /** - * A listener which is notified the initialization of schedule manager. - */ + /** A listener which is notified the initialization of schedule manager. */ public interface OnInitializeListener { - /** - * Called when the schedule manager has been initialized. - */ + /** Called when the schedule manager has been initialized. */ void onInitialize(); } - /** - * A listener which is notified the conflict state change of the schedules. - */ + /** A listener which is notified the conflict state change of the schedules. */ public interface OnConflictStateChangeListener { /** * Called when the conflicting schedules change. - * <p> - * Note that this can be called before - * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called. + * + * <p>Note that this can be called before {@link + * ScheduledRecordingListener#onScheduledRecordingAdded} is called. * * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise - * {@code false}. + * {@code false}. * @param schedules the schedules */ void onConflictStateChange(boolean conflict, ScheduledRecording... schedules); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java index 2d41d732..ed8d6903 100644 --- a/src/com/android/tv/dvr/DvrStorageStatusManager.java +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -11,291 +11,55 @@ * 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 + * limitations under the License. */ - package com.android.tv.dvr; -import android.content.BroadcastReceiver; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.OperationApplicationException; import android.database.Cursor; -import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.AsyncTask; -import android.os.Environment; -import android.os.Looper; import android.os.RemoteException; -import android.os.StatFs; -import android.support.annotation.AnyThread; -import android.support.annotation.IntDef; -import android.support.annotation.WorkerThread; +import android.support.media.tv.TvContractCompat; import android.util.Log; - -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.tuner.tvinput.TunerTvInputService; +import com.android.tv.TvSingletons; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.util.CommonUtils; import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.Utils; - 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.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -/** - * Signals DVR storage status change such as plugging/unplugging. - */ -public class DvrStorageStatusManager { +/** A class for extending TV app-specific function to {@link RecordingStorageStatusManager}. */ +public class DvrStorageStatusManager extends RecordingStorageStatusManager { private static final String TAG = "DvrStorageStatusManager"; - private static final boolean DEBUG = false; - - /** - * Minimum storage size to support DVR - */ - public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB - private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES - = 10 * 1024 * 1024 * 1024L; // 10GB - private static final String RECORDING_DATA_SUB_PATH = "/recording"; - - private static final String[] PROJECTION = { - TvContract.RecordedPrograms._ID, - TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, - TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI - }; - private final static int BATCH_OPERATION_COUNT = 100; - - @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL, - STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING}) - @Retention(RetentionPolicy.SOURCE) - public @interface StorageStatus { - } - - /** - * Current storage is OK to record a program. - */ - public static final int STORAGE_STATUS_OK = 0; - - /** - * Current storage's total capacity is smaller than DVR requirement. - */ - public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1; - - /** - * Current storage's free space is insufficient to record programs. - */ - public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2; - - /** - * Current storage is missing. - */ - public static final int STORAGE_STATUS_MISSING = 3; private final Context mContext; - private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners = - new CopyOnWriteArraySet<>(); - private final boolean mRunningInMainProcess; - private MountedStorageStatus mMountedStorageStatus; - private boolean mStorageValid; private CleanUpDbTask mCleanUpDbTask; - private class MountedStorageStatus { - private final boolean mStorageMounted; - private final File mStorageMountedDir; - private final long mStorageMountedCapacity; - - private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) { - mStorageMounted = mounted; - mStorageMountedDir = mountedDir; - mStorageMountedCapacity = capacity; - } - - private boolean isValidForDvr() { - return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof MountedStorageStatus)) { - return false; - } - MountedStorageStatus status = (MountedStorageStatus) other; - return mStorageMounted == status.mStorageMounted - && Objects.equals(mStorageMountedDir, status.mStorageMountedDir) - && mStorageMountedCapacity == status.mStorageMountedCapacity; - } - } - - public interface OnStorageMountChangedListener { - - /** - * Listener for DVR storage status change. - * - * @param storageMounted {@code true} when DVR possible storage is mounted, - * {@code false} otherwise. - */ - void onStorageMountChanged(boolean storageMounted); - } - - private final class StorageStatusBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - MountedStorageStatus result = getStorageStatusInternal(); - if (mMountedStorageStatus.equals(result)) { - return; - } - mMountedStorageStatus = result; - if (result.mStorageMounted && mRunningInMainProcess) { - // Cleans up DB in LC process. - // Tuner process is not always on. - if (mCleanUpDbTask != null) { - mCleanUpDbTask.cancel(true); - } - mCleanUpDbTask = new CleanUpDbTask(); - mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - boolean valid = result.isValidForDvr(); - if (valid == mStorageValid) { - return; - } - mStorageValid = valid; - for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) { - l.onStorageMountChanged(valid); - } - } - } + private static final String[] PROJECTION = { + TvContractCompat.RecordedPrograms._ID, + TvContractCompat.RecordedPrograms.COLUMN_PACKAGE_NAME, + TvContractCompat.RecordedPrograms.COLUMN_RECORDING_DATA_URI + }; + private static final int BATCH_OPERATION_COUNT = 100; - /** - * Creates DvrStorageStatusManager. - * - * @param context {@link Context} - */ - public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) { + public DvrStorageStatusManager(Context context) { + super(context); mContext = context; - mRunningInMainProcess = runningInMainProcess; - mMountedStorageStatus = getStorageStatusInternal(); - mStorageValid = mMountedStorageStatus.isValidForDvr(); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_MEDIA_MOUNTED); - filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); - filter.addAction(Intent.ACTION_MEDIA_EJECT); - filter.addAction(Intent.ACTION_MEDIA_REMOVED); - filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); - filter.addDataScheme(ContentResolver.SCHEME_FILE); - mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter); } - /** - * Adds the listener for receiving storage status change. - * - * @param listener - */ - public void addListener(OnStorageMountChangedListener listener) { - mOnStorageMountChangedListeners.add(listener); - } - - /** - * Removes the current listener. - */ - public void removeListener(OnStorageMountChangedListener listener) { - mOnStorageMountChangedListeners.remove(listener); - } - - /** - * Returns true if a storage is mounted. - */ - public boolean isStorageMounted() { - return mMountedStorageStatus.mStorageMounted; - } - - /** - * Returns the path to DVR recording data directory. - * This can take for a while sometimes. - */ - @WorkerThread - public File getRecordingRootDataDirectory() { - SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); - if (mMountedStorageStatus.mStorageMountedDir == null) { - return null; - } - File root = mContext.getExternalFilesDir(null); - String rootPath; - try { - rootPath = root != null ? root.getCanonicalPath() : null; - } catch (IOException | SecurityException e) { - return null; - } - return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH); - } - - /** - * Returns the current storage status for DVR recordings. - * - * @return {@link StorageStatus} - */ - @AnyThread - public @StorageStatus int getDvrStorageStatus() { - MountedStorageStatus status = mMountedStorageStatus; - if (status.mStorageMountedDir == null) { - return STORAGE_STATUS_MISSING; - } - if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) { - return STORAGE_STATUS_OK; - } - if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) { - return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL; - } - try { - StatFs statFs = new StatFs(status.mStorageMountedDir.toString()); - if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { - return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; - } - } catch (IllegalArgumentException e) { - // In rare cases, storage status change was not notified yet. - SoftPreconditions.checkState(false); - return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; - } - return STORAGE_STATUS_OK; - } - - /** - * Returns whether the storage has sufficient storage. - * - * @return {@code true} when there is sufficient storage, {@code false} otherwise - */ - public boolean isStorageSufficient() { - return getDvrStorageStatus() == STORAGE_STATUS_OK; - } - - private MountedStorageStatus getStorageStatusInternal() { - boolean storageMounted = - Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); - File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null; - storageMounted = storageMounted && storageMountedDir != null; - long storageMountedCapacity = 0L; - if (storageMounted) { - try { - StatFs statFs = new StatFs(storageMountedDir.toString()); - storageMountedCapacity = statFs.getTotalBytes(); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Storage mount status was changed."); - storageMounted = false; - storageMountedDir = null; - } + @Override + protected void cleanUpDbIfNeeded() { + if (mCleanUpDbTask != null) { + mCleanUpDbTask.cancel(true); } - return new MountedStorageStatus( - storageMounted, storageMountedDir, storageMountedCapacity); + mCleanUpDbTask = new CleanUpDbTask(); + mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> { @@ -307,26 +71,29 @@ public class DvrStorageStatusManager { @Override protected Boolean doInBackground(Void... params) { - @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); - if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + @StorageStatus int storageStatus = getDvrStorageStatus(); + if (storageStatus == STORAGE_STATUS_MISSING) { return null; } - if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + if (storageStatus == STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { return true; } List<ContentProviderOperation> ops = getDeleteOps(); if (ops == null || ops.isEmpty()) { return null; } - Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : " - + ops.size()); - for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) { - int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size() - ? ops.size() : (i + BATCH_OPERATION_COUNT); + Log.i( + TAG, + "New device storage mounted. # of recordings to be forgotten : " + ops.size()); + for (int i = 0; i < ops.size() && !isCancelled(); i += BATCH_OPERATION_COUNT) { + int toIndex = + (i + BATCH_OPERATION_COUNT) > ops.size() + ? ops.size() + : (i + BATCH_OPERATION_COUNT); ArrayList<ContentProviderOperation> batchOps = new ArrayList<>(ops.subList(i, toIndex)); try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps); + mContext.getContentResolver().applyBatch(TvContractCompat.AUTHORITY, batchOps); } catch (RemoteException | OperationApplicationException e) { Log.e(TAG, "Failed to clean up RecordedPrograms.", e); } @@ -337,16 +104,16 @@ public class DvrStorageStatusManager { @Override protected void onPostExecute(Boolean forgetStorage) { if (forgetStorage != null && forgetStorage == true) { - DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(mContext).getDvrManager(); TvInputManagerHelper tvInputManagerHelper = - TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + TvSingletons.getSingletons(mContext).getTvInputManagerHelper(); List<TvInputInfo> tvInputInfoList = tvInputManagerHelper.getTvInputInfos(true, false); if (tvInputInfoList == null || tvInputInfoList.isEmpty()) { return; } for (TvInputInfo info : tvInputInfoList) { - if (Utils.isBundledInput(info.getId())) { + if (CommonUtils.isBundledInput(info.getId())) { dvrManager.forgetStorage(info.getId()); } } @@ -359,16 +126,19 @@ public class DvrStorageStatusManager { private List<ContentProviderOperation> getDeleteOps() { List<ContentProviderOperation> ops = new ArrayList<>(); - try (Cursor c = mContentResolver.query( - TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) { + try (Cursor c = + mContentResolver.query( + TvContractCompat.RecordedPrograms.CONTENT_URI, + PROJECTION, + null, + null, + null)) { if (c == null) { return null; } while (c.moveToNext()) { - @DvrStorageStatusManager.StorageStatus int storageStatus = - getDvrStorageStatus(); - if (isCancelled() - || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + @StorageStatus int storageStatus = getDvrStorageStatus(); + if (isCancelled() || storageStatus == STORAGE_STATUS_MISSING) { ops.clear(); break; } @@ -379,15 +149,19 @@ public class DvrStorageStatusManager { continue; } Uri dataUri = Uri.parse(dataUriString); - if (!Utils.isInBundledPackageSet(packageName) - || dataUri == null || dataUri.getPath() == null + if (!CommonUtils.isInBundledPackageSet(packageName) + || dataUri == null + || dataUri.getPath() == null || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { continue; } File recordedProgramDir = new File(dataUri.getPath()); if (!recordedProgramDir.exists()) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); + ops.add( + ContentProviderOperation.newDelete( + TvContractCompat.buildRecordedProgramUri( + Long.parseLong(id))) + .build()); } } return ops; diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index da6ddb1a..8616962f 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -20,10 +20,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.media.tv.TvInputManager; import android.support.annotation.IntDef; - -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.util.SharedPreferencesUtils; import com.android.tv.dvr.data.RecordedProgram; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -33,8 +31,8 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; /** - * A class to manage DVR watched state. - * It will remember and provides previous watched position of DVR playback. + * A class to manage DVR watched state. It will remember and provides previous watched position of + * DVR playback. */ public class DvrWatchedPositionManager { private SharedPreferences mWatchedPositions; @@ -49,55 +47,46 @@ public class DvrWatchedPositionManager { @Retention(RetentionPolicy.SOURCE) @IntDef({DVR_WATCHED_STATUS_NEW, DVR_WATCHED_STATUS_WATCHING, DVR_WATCHED_STATUS_WATCHED}) public @interface DvrWatchedStatus {} - /** - * The status indicates the recorded program has not been watched at all. - */ + /** The status indicates the recorded program has not been watched at all. */ public static final int DVR_WATCHED_STATUS_NEW = 0; - /** - * The status indicates the recorded program is being watched. - */ + /** The status indicates the recorded program is being watched. */ public static final int DVR_WATCHED_STATUS_WATCHING = 1; - /** - * The status indicates the recorded program was completely watched. - */ + /** The status indicates the recorded program was completely watched. */ public static final int DVR_WATCHED_STATUS_WATCHED = 2; public DvrWatchedPositionManager(Context context) { - mWatchedPositions = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); + mWatchedPositions = + context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_DVR_WATCHED_POSITION, + Context.MODE_PRIVATE); } - /** - * Sets the watched position of the give program. - */ + /** Sets the watched position of the give program. */ public void setWatchedPosition(long recordedProgramId, long positionMs) { mWatchedPositions.edit().putLong(Long.toString(recordedProgramId), positionMs).apply(); notifyWatchedPositionChanged(recordedProgramId, positionMs); } - /** - * Gets the watched position of the give program. - */ + /** Gets the watched position of the give program. */ public long getWatchedPosition(long recordedProgramId) { - return mWatchedPositions.getLong(Long.toString(recordedProgramId), - TvInputManager.TIME_SHIFT_INVALID_TIME); + return mWatchedPositions.getLong( + Long.toString(recordedProgramId), TvInputManager.TIME_SHIFT_INVALID_TIME); } - @DvrWatchedStatus public int getWatchedStatus(RecordedProgram recordedProgram) { + @DvrWatchedStatus + public int getWatchedStatus(RecordedProgram recordedProgram) { long watchedPosition = getWatchedPosition(recordedProgram.getId()); if (watchedPosition == TvInputManager.TIME_SHIFT_INVALID_TIME) { return DVR_WATCHED_STATUS_NEW; - } else if (watchedPosition > recordedProgram - .getDurationMillis() * DVR_WATCHED_THRESHOLD_RATE) { + } else if (watchedPosition + > recordedProgram.getDurationMillis() * DVR_WATCHED_THRESHOLD_RATE) { return DVR_WATCHED_STATUS_WATCHED; } else { return DVR_WATCHED_STATUS_WATCHING; } } - /** - * Adds {@link WatchedPositionChangedListener}. - */ + /** Adds {@link WatchedPositionChangedListener}. */ public void addListener(WatchedPositionChangedListener listener, long recordedProgramId) { if (recordedProgramId == RecordedProgram.ID_NOT_SET) { return; @@ -110,18 +99,14 @@ public class DvrWatchedPositionManager { listenerSet.add(listener); } - /** - * Removes {@link WatchedPositionChangedListener}. - */ + /** Removes {@link WatchedPositionChangedListener}. */ public void removeListener(WatchedPositionChangedListener listener) { for (long recordedProgramId : new ArrayList<>(mListeners.keySet())) { removeListener(listener, recordedProgramId); } } - /** - * Removes {@link WatchedPositionChangedListener}. - */ + /** Removes {@link WatchedPositionChangedListener}. */ public void removeListener(WatchedPositionChangedListener listener, long recordedProgramId) { Set<WatchedPositionChangedListener> listenerSet = mListeners.get(recordedProgramId); if (listenerSet == null) { @@ -144,9 +129,7 @@ public class DvrWatchedPositionManager { } public interface WatchedPositionChangedListener { - /** - * Called when the watched position of some program is changed. - */ + /** Called when the watched position of some program is changed. */ void onWatchedPositionChanged(long recordedProgramId, long positionMs); } } diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 129ba153..1b505e80 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -17,7 +17,6 @@ package com.android.tv.dvr; import android.support.annotation.MainThread; - import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.RecordingState; import com.android.tv.dvr.data.SeriesRecording; @@ -30,19 +29,13 @@ import com.android.tv.dvr.data.SeriesRecording; */ @MainThread public interface WritableDvrDataManager extends DvrDataManager { - /** - * Adds new recordings. - */ + /** Adds new recordings. */ void addScheduledRecording(ScheduledRecording... scheduledRecordings); - /** - * Adds new series recordings. - */ + /** Adds new series recordings. */ void addSeriesRecording(SeriesRecording... seriesRecordings); - /** - * Removes recordings. - */ + /** Removes recordings. */ void removeScheduledRecording(ScheduledRecording... scheduledRecordings); /** @@ -51,30 +44,30 @@ public interface WritableDvrDataManager extends DvrDataManager { */ void removeScheduledRecording(boolean forceRemove, ScheduledRecording... scheduledRecordings); - /** - * Removes series recordings. - */ + /** Removes series recordings. */ void removeSeriesRecording(SeriesRecording... seasonSchedules); - /** - * Updates existing recordings. - */ + /** Updates existing recordings. */ void updateScheduledRecording(ScheduledRecording... scheduledRecordings); - /** - * Updates existing series recordings. - */ + /** Updates existing series recordings. */ void updateSeriesRecording(SeriesRecording... seriesRecordings); + /** Changes the state of the recording. */ + void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState); + /** * Changes the state of the recording. + * + * @param reason the reason of this change */ - void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState); + void changeState( + ScheduledRecording scheduledRecording, @RecordingState int newState, int reason); /** * Remove all the records related to the input. - * <p> - * Note that this should be called after the input was removed. + * + * <p>Note that this should be called after the input was removed. */ void forgetStorage(String inputId); } diff --git a/src/com/android/tv/dvr/data/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java index 2ade1dad..496651ba 100644 --- a/src/com/android/tv/dvr/data/IdGenerator.java +++ b/src/com/android/tv/dvr/data/IdGenerator.java @@ -18,32 +18,22 @@ package com.android.tv.dvr.data; import java.util.concurrent.atomic.AtomicLong; -/** - * A class which generate the ID which increases sequentially. - */ +/** A class which generate the ID which increases sequentially. */ public class IdGenerator { - /** - * ID generator for the scheduled recording. - */ + /** ID generator for the scheduled recording. */ public static final IdGenerator SCHEDULED_RECORDING = new IdGenerator(); - /** - * ID generator for the series recording. - */ + /** ID generator for the series recording. */ public static final IdGenerator SERIES_RECORDING = new IdGenerator(); private final AtomicLong mMaxId = new AtomicLong(0); - /** - * Sets the new maximum ID. - */ + /** Sets the new maximum ID. */ public void setMaxId(long maxId) { mMaxId.set(maxId); } - /** - * Returns the new ID which is greater than the existing maximum ID by 1. - */ + /** Returns the new ID which is greater than the existing maximum ID by 1. */ public long newId() { return mMaxId.incrementAndGet(); } diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java index 2e953a52..e1fbca8c 100644 --- a/src/com/android/tv/dvr/data/RecordedProgram.java +++ b/src/com/android/tv/dvr/data/RecordedProgram.java @@ -28,99 +28,97 @@ import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.common.R; import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.BaseProgram; import com.android.tv.data.GenreItems; import com.android.tv.data.InternalDataUtils; -import com.android.tv.util.Utils; - import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.Objects; import java.util.concurrent.TimeUnit; -/** - * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. - */ +/** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */ @TargetApi(Build.VERSION_CODES.N) public class RecordedProgram extends BaseProgram { public static final int ID_NOT_SET = -1; - public final static String[] PROJECTION = { - // These are in exactly the order listed in RecordedPrograms - RecordedPrograms._ID, - RecordedPrograms.COLUMN_PACKAGE_NAME, - RecordedPrograms.COLUMN_INPUT_ID, - RecordedPrograms.COLUMN_CHANNEL_ID, - RecordedPrograms.COLUMN_TITLE, - RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, - RecordedPrograms.COLUMN_SEASON_TITLE, - RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, - RecordedPrograms.COLUMN_EPISODE_TITLE, - RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_BROADCAST_GENRE, - RecordedPrograms.COLUMN_CANONICAL_GENRE, - RecordedPrograms.COLUMN_SHORT_DESCRIPTION, - RecordedPrograms.COLUMN_LONG_DESCRIPTION, - RecordedPrograms.COLUMN_VIDEO_WIDTH, - RecordedPrograms.COLUMN_VIDEO_HEIGHT, - RecordedPrograms.COLUMN_AUDIO_LANGUAGE, - RecordedPrograms.COLUMN_CONTENT_RATING, - RecordedPrograms.COLUMN_POSTER_ART_URI, - RecordedPrograms.COLUMN_THUMBNAIL_URI, - RecordedPrograms.COLUMN_SEARCHABLE, - RecordedPrograms.COLUMN_RECORDING_DATA_URI, - RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, - RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, - RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, - RecordedPrograms.COLUMN_VERSION_NUMBER, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + public static final String[] PROJECTION = { + // These are in exactly the order listed in RecordedPrograms + RecordedPrograms._ID, + RecordedPrograms.COLUMN_PACKAGE_NAME, + RecordedPrograms.COLUMN_INPUT_ID, + RecordedPrograms.COLUMN_CHANNEL_ID, + RecordedPrograms.COLUMN_TITLE, + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_SEASON_TITLE, + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_EPISODE_TITLE, + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_BROADCAST_GENRE, + RecordedPrograms.COLUMN_CANONICAL_GENRE, + RecordedPrograms.COLUMN_SHORT_DESCRIPTION, + RecordedPrograms.COLUMN_LONG_DESCRIPTION, + RecordedPrograms.COLUMN_VIDEO_WIDTH, + RecordedPrograms.COLUMN_VIDEO_HEIGHT, + RecordedPrograms.COLUMN_AUDIO_LANGUAGE, + RecordedPrograms.COLUMN_CONTENT_RATING, + RecordedPrograms.COLUMN_POSTER_ART_URI, + RecordedPrograms.COLUMN_THUMBNAIL_URI, + RecordedPrograms.COLUMN_SEARCHABLE, + RecordedPrograms.COLUMN_RECORDING_DATA_URI, + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + RecordedPrograms.COLUMN_VERSION_NUMBER, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, }; public static RecordedProgram fromCursor(Cursor cursor) { int index = 0; - Builder builder = builder() - .setId(cursor.getLong(index++)) - .setPackageName(cursor.getString(index++)) - .setInputId(cursor.getString(index++)) - .setChannelId(cursor.getLong(index++)) - .setTitle(cursor.getString(index++)) - .setSeasonNumber(cursor.getString(index++)) - .setSeasonTitle(cursor.getString(index++)) - .setEpisodeNumber(cursor.getString(index++)) - .setEpisodeTitle(cursor.getString(index++)) - .setStartTimeUtcMillis(cursor.getLong(index++)) - .setEndTimeUtcMillis(cursor.getLong(index++)) - .setBroadcastGenres(cursor.getString(index++)) - .setCanonicalGenres(cursor.getString(index++)) - .setShortDescription(cursor.getString(index++)) - .setLongDescription(cursor.getString(index++)) - .setVideoWidth(cursor.getInt(index++)) - .setVideoHeight(cursor.getInt(index++)) - .setAudioLanguage(cursor.getString(index++)) - .setContentRatings( - TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))) - .setPosterArtUri(cursor.getString(index++)) - .setThumbnailUri(cursor.getString(index++)) - .setSearchable(cursor.getInt(index++) == 1) - .setDataUri(cursor.getString(index++)) - .setDataBytes(cursor.getLong(index++)) - .setDurationMillis(cursor.getLong(index++)) - .setExpireTimeUtcMillis(cursor.getLong(index++)) - .setInternalProviderFlag1(cursor.getInt(index++)) - .setInternalProviderFlag2(cursor.getInt(index++)) - .setInternalProviderFlag3(cursor.getInt(index++)) - .setInternalProviderFlag4(cursor.getInt(index++)) - .setVersionNumber(cursor.getInt(index++)); - if (Utils.isInBundledPackageSet(builder.mPackageName)) { + Builder builder = + builder() + .setId(cursor.getLong(index++)) + .setPackageName(cursor.getString(index++)) + .setInputId(cursor.getString(index++)) + .setChannelId(cursor.getLong(index++)) + .setTitle(cursor.getString(index++)) + .setSeasonNumber(cursor.getString(index++)) + .setSeasonTitle(cursor.getString(index++)) + .setEpisodeNumber(cursor.getString(index++)) + .setEpisodeTitle(cursor.getString(index++)) + .setStartTimeUtcMillis(cursor.getLong(index++)) + .setEndTimeUtcMillis(cursor.getLong(index++)) + .setBroadcastGenres(cursor.getString(index++)) + .setCanonicalGenres(cursor.getString(index++)) + .setShortDescription(cursor.getString(index++)) + .setLongDescription(cursor.getString(index++)) + .setVideoWidth(cursor.getInt(index++)) + .setVideoHeight(cursor.getInt(index++)) + .setAudioLanguage(cursor.getString(index++)) + .setContentRatings( + TvContentRatingCache.getInstance() + .getRatings(cursor.getString(index++))) + .setPosterArtUri(cursor.getString(index++)) + .setThumbnailUri(cursor.getString(index++)) + .setSearchable(cursor.getInt(index++) == 1) + .setDataUri(cursor.getString(index++)) + .setDataBytes(cursor.getLong(index++)) + .setDurationMillis(cursor.getLong(index++)) + .setExpireTimeUtcMillis(cursor.getLong(index++)) + .setInternalProviderFlag1(cursor.getInt(index++)) + .setInternalProviderFlag2(cursor.getInt(index++)) + .setInternalProviderFlag3(cursor.getInt(index++)) + .setInternalProviderFlag4(cursor.getInt(index++)) + .setVersionNumber(cursor.getInt(index++)); + if (CommonUtils.isInBundledPackageSet(builder.mPackageName)) { InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); } return builder.build(); @@ -138,12 +136,14 @@ public class RecordedProgram extends BaseProgram { values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle); values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber); values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle); - values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, - recordedProgram.mStartTimeUtcMillis); + values.put( + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, recordedProgram.mStartTimeUtcMillis); values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, + values.put( + RecordedPrograms.COLUMN_BROADCAST_GENRE, safeEncode(recordedProgram.mBroadcastGenres)); - values.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, + values.put( + RecordedPrograms.COLUMN_CANONICAL_GENRE, safeEncode(recordedProgram.mCanonicalGenres)); values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription); values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription); @@ -158,33 +158,40 @@ public class RecordedProgram extends BaseProgram { values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); } values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); - values.put(RecordedPrograms.COLUMN_CONTENT_RATING, + values.put( + RecordedPrograms.COLUMN_CONTENT_RATING, TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings)); values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri); values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri); values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0); - values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, - safeToString(recordedProgram.mDataUri)); + values.put( + RecordedPrograms.COLUMN_RECORDING_DATA_URI, safeToString(recordedProgram.mDataUri)); values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes); - values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, - recordedProgram.mDurationMillis); - values.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, + values.put( + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, recordedProgram.mDurationMillis); + values.put( + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, recordedProgram.mExpireTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + values.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(recordedProgram)); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + values.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, recordedProgram.mInternalProviderFlag1); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + values.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, recordedProgram.mInternalProviderFlag2); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + values.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, recordedProgram.mInternalProviderFlag3); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + values.put( + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, recordedProgram.mInternalProviderFlag4); values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber); return values; } - public static class Builder{ + public static class Builder { private long mId = ID_NOT_SET; private String mPackageName; private String mInputId; @@ -414,18 +421,45 @@ public class RecordedProgram extends BaseProgram { // If series ID is not set, generate it for the episodic program of other TV input. setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle)); } - return new RecordedProgram(mId, mPackageName, mInputId, mChannelId, mTitle, mSeriesId, - mSeasonNumber, mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis, - mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription, - mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRatings, - mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes, - mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1, - mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4, + return new RecordedProgram( + mId, + mPackageName, + mInputId, + mChannelId, + mTitle, + mSeriesId, + mSeasonNumber, + mSeasonTitle, + mEpisodeNumber, + mEpisodeTitle, + mStartTimeUtcMillis, + mEndTimeUtcMillis, + mBroadcastGenres, + mCanonicalGenres, + mShortDescription, + mLongDescription, + mVideoWidth, + mVideoHeight, + mAudioLanguage, + mContentRatings, + mPosterArtUri, + mThumbnailUri, + mSearchable, + mDataUri, + mDataBytes, + mDurationMillis, + mExpireTimeUtcMillis, + mInternalProviderFlag1, + mInternalProviderFlag2, + mInternalProviderFlag3, + mInternalProviderFlag4, mVersionNumber); } } - public static Builder builder() { return new Builder(); } + public static Builder builder() { + return new Builder(); + } public static Builder buildFrom(RecordedProgram orig) { return builder() @@ -470,7 +504,7 @@ public class RecordedProgram extends BaseProgram { } return Long.compare(lhs.mId, rhs.mId); } - }; + }; private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); @@ -507,15 +541,38 @@ public class RecordedProgram extends BaseProgram { private final int mInternalProviderFlag4; private final int mVersionNumber; - private RecordedProgram(long id, String packageName, String inputId, long channelId, - String title, String seriesId, String seasonNumber, String seasonTitle, - String episodeNumber, String episodeTitle, long startTimeUtcMillis, - long endTimeUtcMillis, String[] broadcastGenres, String[] canonicalGenres, - String shortDescription, String longDescription, int videoWidth, int videoHeight, - String audioLanguage, TvContentRating[] contentRatings, String posterArtUri, - String thumbnailUri, boolean searchable, Uri dataUri, long dataBytes, - long durationMillis, long expireTimeUtcMillis, int internalProviderFlag1, - int internalProviderFlag2, int internalProviderFlag3, int internalProviderFlag4, + private RecordedProgram( + long id, + String packageName, + String inputId, + long channelId, + String title, + String seriesId, + String seasonNumber, + String seasonTitle, + String episodeNumber, + String episodeTitle, + long startTimeUtcMillis, + long endTimeUtcMillis, + String[] broadcastGenres, + String[] canonicalGenres, + String shortDescription, + String longDescription, + int videoWidth, + int videoHeight, + String audioLanguage, + TvContentRating[] contentRatings, + String posterArtUri, + String thumbnailUri, + boolean searchable, + Uri dataUri, + long dataBytes, + long durationMillis, + long expireTimeUtcMillis, + int internalProviderFlag1, + int internalProviderFlag2, + int internalProviderFlag3, + int internalProviderFlag4, int versionNumber) { mId = id; mPackageName = packageName; @@ -564,9 +621,7 @@ public class RecordedProgram extends BaseProgram { return mCanonicalGenres; } - /** - * Returns array of canonical genre ID's for this recorded program. - */ + /** Returns array of canonical genre ID's for this recorded program. */ @Override public int[] getCanonicalGenreIds() { if (mCanonicalGenres == null) { @@ -623,11 +678,15 @@ public class RecordedProgram extends BaseProgram { if (!TextUtils.isEmpty(mEpisodeNumber)) { if (TextUtils.equals(mSeasonNumber, "0")) { // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_number_format_no_season_number), mEpisodeNumber); + return String.format( + context.getResources() + .getString(R.string.display_episode_number_format_no_season_number), + mEpisodeNumber); } else { - return String.format(context.getResources().getString( - R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber); + return String.format( + context.getResources().getString(R.string.display_episode_number_format), + mSeasonNumber, + mEpisodeNumber); } } return null; @@ -734,9 +793,7 @@ public class RecordedProgram extends BaseProgram { return mVideoWidth; } - /** - * Checks whether the recording has been clipped or not. - */ + /** Checks whether the recording has been clipped or not. */ public boolean isClipped() { return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS; } @@ -746,40 +803,38 @@ public class RecordedProgram extends BaseProgram { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RecordedProgram that = (RecordedProgram) o; - return Objects.equals(mId, that.mId) && - Objects.equals(mChannelId, that.mChannelId) && - Objects.equals(mSeriesId, that.mSeriesId) && - Objects.equals(mSeasonNumber, that.mSeasonNumber) && - Objects.equals(mSeasonTitle, that.mSeasonTitle) && - Objects.equals(mEpisodeNumber, that.mEpisodeNumber) && - Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) && - Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) && - Objects.equals(mVideoWidth, that.mVideoWidth) && - Objects.equals(mVideoHeight, that.mVideoHeight) && - Objects.equals(mSearchable, that.mSearchable) && - Objects.equals(mDataBytes, that.mDataBytes) && - Objects.equals(mDurationMillis, that.mDurationMillis) && - Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) && - Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) && - Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) && - Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) && - Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) && - Objects.equals(mVersionNumber, that.mVersionNumber) && - Objects.equals(mTitle, that.mTitle) && - Objects.equals(mEpisodeTitle, that.mEpisodeTitle) && - Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) && - Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) && - Objects.equals(mShortDescription, that.mShortDescription) && - Objects.equals(mLongDescription, that.mLongDescription) && - Objects.equals(mAudioLanguage, that.mAudioLanguage) && - Arrays.equals(mContentRatings, that.mContentRatings) && - Objects.equals(mPosterArtUri, that.mPosterArtUri) && - Objects.equals(mThumbnailUri, that.mThumbnailUri); - } - - /** - * Hashes based on the ID. - */ + return Objects.equals(mId, that.mId) + && Objects.equals(mChannelId, that.mChannelId) + && Objects.equals(mSeriesId, that.mSeriesId) + && Objects.equals(mSeasonNumber, that.mSeasonNumber) + && Objects.equals(mSeasonTitle, that.mSeasonTitle) + && Objects.equals(mEpisodeNumber, that.mEpisodeNumber) + && Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) + && Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) + && Objects.equals(mVideoWidth, that.mVideoWidth) + && Objects.equals(mVideoHeight, that.mVideoHeight) + && Objects.equals(mSearchable, that.mSearchable) + && Objects.equals(mDataBytes, that.mDataBytes) + && Objects.equals(mDurationMillis, that.mDurationMillis) + && Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) + && Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) + && Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) + && Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) + && Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) + && Objects.equals(mVersionNumber, that.mVersionNumber) + && Objects.equals(mTitle, that.mTitle) + && Objects.equals(mEpisodeTitle, that.mEpisodeTitle) + && Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) + && Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) + && Objects.equals(mShortDescription, that.mShortDescription) + && Objects.equals(mLongDescription, that.mLongDescription) + && Objects.equals(mAudioLanguage, that.mAudioLanguage) + && Arrays.equals(mContentRatings, that.mContentRatings) + && Objects.equals(mPosterArtUri, that.mPosterArtUri) + && Objects.equals(mThumbnailUri, that.mThumbnailUri); + } + + /** Hashes based on the ID. */ @Override public int hashCode() { return Objects.hash(mId); @@ -788,42 +843,80 @@ public class RecordedProgram extends BaseProgram { @Override public String toString() { return "RecordedProgram" - + "[" + mId + - "]{ mPackageName=" + mPackageName + - ", mInputId='" + mInputId + '\'' + - ", mChannelId='" + mChannelId + '\'' + - ", mTitle='" + mTitle + '\'' + - ", mSeriesId='" + mSeriesId + '\'' + - ", mEpisodeNumber=" + mEpisodeNumber + - ", mEpisodeTitle='" + mEpisodeTitle + '\'' + - ", mStartTimeUtcMillis=" + mStartTimeUtcMillis + - ", mEndTimeUtcMillis=" + mEndTimeUtcMillis + - ", mBroadcastGenres=" + - (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") + - ", mCanonicalGenres=" + - (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") + - ", mShortDescription='" + mShortDescription + '\'' + - ", mLongDescription='" + mLongDescription + '\'' + - ", mVideoHeight=" + mVideoHeight + - ", mVideoWidth=" + mVideoWidth + - ", mAudioLanguage='" + mAudioLanguage + '\'' + - ", mContentRatings='" + - TvContentRatingCache.contentRatingsToString(mContentRatings) + '\'' + - ", mPosterArtUri=" + mPosterArtUri + - ", mThumbnailUri=" + mThumbnailUri + - ", mSearchable=" + mSearchable + - ", mDataUri=" + mDataUri + - ", mDataBytes=" + mDataBytes + - ", mDurationMillis=" + mDurationMillis + - ", mExpireTimeUtcMillis=" + mExpireTimeUtcMillis + - ", mInternalProviderFlag1=" + mInternalProviderFlag1 + - ", mInternalProviderFlag2=" + mInternalProviderFlag2 + - ", mInternalProviderFlag3=" + mInternalProviderFlag3 + - ", mInternalProviderFlag4=" + mInternalProviderFlag4 + - ", mSeasonNumber=" + mSeasonNumber + - ", mSeasonTitle=" + mSeasonTitle + - ", mVersionNumber=" + mVersionNumber + - '}'; + + "[" + + mId + + "]{ mPackageName=" + + mPackageName + + ", mInputId='" + + mInputId + + '\'' + + ", mChannelId='" + + mChannelId + + '\'' + + ", mTitle='" + + mTitle + + '\'' + + ", mSeriesId='" + + mSeriesId + + '\'' + + ", mEpisodeNumber=" + + mEpisodeNumber + + ", mEpisodeTitle='" + + mEpisodeTitle + + '\'' + + ", mStartTimeUtcMillis=" + + mStartTimeUtcMillis + + ", mEndTimeUtcMillis=" + + mEndTimeUtcMillis + + ", mBroadcastGenres=" + + (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") + + ", mCanonicalGenres=" + + (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") + + ", mShortDescription='" + + mShortDescription + + '\'' + + ", mLongDescription='" + + mLongDescription + + '\'' + + ", mVideoHeight=" + + mVideoHeight + + ", mVideoWidth=" + + mVideoWidth + + ", mAudioLanguage='" + + mAudioLanguage + + '\'' + + ", mContentRatings='" + + TvContentRatingCache.contentRatingsToString(mContentRatings) + + '\'' + + ", mPosterArtUri=" + + mPosterArtUri + + ", mThumbnailUri=" + + mThumbnailUri + + ", mSearchable=" + + mSearchable + + ", mDataUri=" + + mDataUri + + ", mDataBytes=" + + mDataBytes + + ", mDurationMillis=" + + mDurationMillis + + ", mExpireTimeUtcMillis=" + + mExpireTimeUtcMillis + + ", mInternalProviderFlag1=" + + mInternalProviderFlag1 + + ", mInternalProviderFlag2=" + + mInternalProviderFlag2 + + ", mInternalProviderFlag3=" + + mInternalProviderFlag3 + + ", mInternalProviderFlag4=" + + mInternalProviderFlag4 + + ", mSeasonNumber=" + + mSeasonNumber + + ", mSeasonTitle=" + + mSeasonTitle + + ", mVersionNumber=" + + mVersionNumber + + '}'; } @Nullable @@ -836,9 +929,7 @@ public class RecordedProgram extends BaseProgram { return genres == null ? null : TvContract.Programs.Genres.encode(genres); } - /** - * Returns an array containing all of the elements in the list. - */ + /** Returns an array containing all of the elements in the list. */ public static RecordedProgram[] toArray(Collection<RecordedProgram> recordedPrograms) { return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]); } diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java index 5d11c0f3..7c2d12d9 100644 --- a/src/com/android/tv/dvr/data/ScheduledRecording.java +++ b/src/com/android/tv/dvr/data/ScheduledRecording.java @@ -16,107 +16,97 @@ package com.android.tv.dvr.data; +import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Range; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.util.CompositeComparator; -import com.android.tv.util.Utils; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collection; import java.util.Comparator; import java.util.Objects; -/** - * A data class for one recording contents. - */ +/** A data class for one recording contents. */ +@TargetApi(Build.VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public final class ScheduledRecording implements Parcelable { private static final String TAG = "ScheduledRecording"; - /** - * Indicates that the ID is not assigned yet. - */ + /** Indicates that the ID is not assigned yet. */ public static final long ID_NOT_SET = 0; - /** - * The default priority of the recording. - */ + /** The default priority of the recording. */ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; - /** - * Compares the start time in ascending order. - */ - public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR - = new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); - } - }; - - /** - * Compares the end time in ascending order. - */ - public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR - = new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); - } - }; - - /** - * Compares ID in ascending order. The schedule with the larger ID was created later. - */ - public static final Comparator<ScheduledRecording> ID_COMPARATOR - = new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mId, rhs.mId); - } - }; - - /** - * Compares the priority in ascending order. - */ - public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR - = new Comparator<ScheduledRecording>() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mPriority, rhs.mPriority); - } - }; + /** Compares the start time in ascending order. */ + public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = + new Comparator<ScheduledRecording>() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); + } + }; + + /** Compares the end time in ascending order. */ + public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR = + new Comparator<ScheduledRecording>() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); + } + }; + + /** Compares ID in ascending order. The schedule with the larger ID was created later. */ + public static final Comparator<ScheduledRecording> ID_COMPARATOR = + new Comparator<ScheduledRecording>() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mId, rhs.mId); + } + }; + + /** Compares the priority in ascending order. */ + public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = + new Comparator<ScheduledRecording>() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mPriority, rhs.mPriority); + } + }; /** * Compares start time in ascending order and then priority in descending order and then ID in * descending order. */ - public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(), - ID_COMPARATOR.reversed()); + public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR = + new CompositeComparator<>( + START_TIME_COMPARATOR, + PRIORITY_COMPARATOR.reversed(), + ID_COMPARATOR.reversed()); - /** - * Builds scheduled recordings from programs. - */ + /** Builds scheduled recordings from programs. */ public static Builder builder(String inputId, Program p) { return new Builder() .setInputId(inputId) .setChannelId(p.getChannelId()) - .setStartTimeMs(p.getStartTimeUtcMillis()).setEndTimeMs(p.getEndTimeUtcMillis()) + .setStartTimeMs(p.getStartTimeUtcMillis()) + .setEndTimeMs(p.getEndTimeUtcMillis()) .setProgramId(p.getId()) .setProgramTitle(p.getTitle()) .setSeasonNumber(p.getSeasonNumber()) @@ -138,9 +128,7 @@ public final class ScheduledRecording implements Parcelable { .setType(TYPE_TIMED); } - /** - * Creates a new Builder with the values set from the {@link RecordedProgram}. - */ + /** Creates a new Builder with the values set from the {@link RecordedProgram}. */ public static Builder builder(RecordedProgram p) { boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle()); return new Builder() @@ -157,7 +145,8 @@ public final class ScheduledRecording implements Parcelable { .setProgramLongDescription(p.getLongDescription()) .setProgramPosterArtUri(p.getPosterArtUri()) .setProgramThumbnailUri(p.getThumbnailUri()) - .setState(STATE_RECORDING_FINISHED); + .setState(STATE_RECORDING_FINISHED) + .setRecordedProgramId(p.getId()); } public static final class Builder { @@ -179,8 +168,10 @@ public final class ScheduledRecording implements Parcelable { private String mProgramThumbnailUri; private @RecordingState int mState; private long mSeriesRecordingId = ID_NOT_SET; + private Long mRecodedProgramId; + private Integer mFailedReason; - private Builder() { } + private Builder() {} public Builder setId(long id) { mId = id; @@ -272,17 +263,42 @@ public final class ScheduledRecording implements Parcelable { return this; } + public Builder setRecordedProgramId(Long recordedProgramId) { + mRecodedProgramId = recordedProgramId; + return this; + } + + public Builder setFailedReason(Integer reason) { + mFailedReason = reason; + return this; + } + public ScheduledRecording build() { - return new ScheduledRecording(mId, mPriority, mInputId, mChannelId, mProgramId, - mProgramTitle, mType, mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, - mEpisodeTitle, mProgramDescription, mProgramLongDescription, - mProgramPosterArtUri, mProgramThumbnailUri, mState, mSeriesRecordingId); + return new ScheduledRecording( + mId, + mPriority, + mInputId, + mChannelId, + mProgramId, + mProgramTitle, + mType, + mStartTimeMs, + mEndTimeMs, + mSeasonNumber, + mEpisodeNumber, + mEpisodeTitle, + mProgramDescription, + mProgramLongDescription, + mProgramPosterArtUri, + mProgramThumbnailUri, + mState, + mSeriesRecordingId, + mRecodedProgramId, + mFailedReason); } } - /** - * Creates {@link Builder} object from the given original {@code Recording}. - */ + /** Creates {@link Builder} object from the given original {@code Recording}. */ public static Builder buildFrom(ScheduledRecording orig) { return new Builder() .setId(orig.mId) @@ -301,14 +317,23 @@ public final class ScheduledRecording implements Parcelable { .setProgramLongDescription(orig.getProgramLongDescription()) .setProgramPosterArtUri(orig.getProgramPosterArtUri()) .setProgramThumbnailUri(orig.getProgramThumbnailUri()) - .setState(orig.mState).setType(orig.mType); + .setState(orig.mState) + .setFailedReason(orig.getFailedReason()) + .setType(orig.mType); } @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS, STATE_RECORDING_FINISHED, - STATE_RECORDING_FAILED, STATE_RECORDING_CLIPPED, STATE_RECORDING_DELETED, - STATE_RECORDING_CANCELED}) + @IntDef({ + STATE_RECORDING_NOT_STARTED, + STATE_RECORDING_IN_PROGRESS, + STATE_RECORDING_FINISHED, + STATE_RECORDING_FAILED, + STATE_RECORDING_CLIPPED, + STATE_RECORDING_DELETED, + STATE_RECORDING_CANCELED + }) public @interface RecordingState {} + public static final int STATE_RECORDING_NOT_STARTED = 0; public static final int STATE_RECORDING_IN_PROGRESS = 1; public static final int STATE_RECORDING_FINISHED = 2; @@ -317,48 +342,74 @@ public final class ScheduledRecording implements Parcelable { public static final int STATE_RECORDING_DELETED = 5; public static final int STATE_RECORDING_CANCELED = 6; + /** The reasons of failed recordings */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FAILED_REASON_OTHER, + FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED, + FAILED_REASON_NOT_FINISHED, + FAILED_REASON_SCHEDULER_STOPPED, + FAILED_REASON_INVALID_CHANNEL, + FAILED_REASON_MESSAGE_NOT_SENT, + FAILED_REASON_CONNECTION_FAILED, + FAILED_REASON_RESOURCE_BUSY, + FAILED_REASON_INPUT_UNAVAILABLE, + FAILED_REASON_INPUT_DVR_UNSUPPORTED, + FAILED_REASON_INSUFFICIENT_SPACE + }) + public @interface RecordingFailedReason {} + + public static final int FAILED_REASON_OTHER = 0; + public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1; + public static final int FAILED_REASON_NOT_FINISHED = 2; + public static final int FAILED_REASON_SCHEDULER_STOPPED = 3; + public static final int FAILED_REASON_INVALID_CHANNEL = 4; + public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5; + public static final int FAILED_REASON_CONNECTION_FAILED = 6; + public static final int FAILED_REASON_RESOURCE_BUSY = 7; + // For the following reasons, show advice to users + public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8; + public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9; + public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10; + @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_TIMED, TYPE_PROGRAM}) public @interface RecordingType {} - /** - * Record with given time range. - */ + /** Record with given time range. */ public static final int TYPE_TIMED = 1; - /** - * Record with a given program. - */ + /** Record with a given program. */ public static final int TYPE_PROGRAM = 2; @RecordingType private final int mType; /** - * Use this projection if you want to create {@link ScheduledRecording} object using - * {@link #fromCursor}. + * Use this projection if you want to create {@link ScheduledRecording} object using {@link + * #fromCursor}. */ public static final String[] PROJECTION = { - // Columns must match what is read in #fromCursor - Schedules._ID, - Schedules.COLUMN_PRIORITY, - Schedules.COLUMN_TYPE, - Schedules.COLUMN_INPUT_ID, - Schedules.COLUMN_CHANNEL_ID, - Schedules.COLUMN_PROGRAM_ID, - Schedules.COLUMN_PROGRAM_TITLE, - Schedules.COLUMN_START_TIME_UTC_MILLIS, - Schedules.COLUMN_END_TIME_UTC_MILLIS, - Schedules.COLUMN_SEASON_NUMBER, - Schedules.COLUMN_EPISODE_NUMBER, - Schedules.COLUMN_EPISODE_TITLE, - Schedules.COLUMN_PROGRAM_DESCRIPTION, - Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, - Schedules.COLUMN_PROGRAM_POST_ART_URI, - Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, - Schedules.COLUMN_STATE, - Schedules.COLUMN_SERIES_RECORDING_ID}; + // Columns must match what is read in #fromCursor + Schedules._ID, + Schedules.COLUMN_PRIORITY, + Schedules.COLUMN_TYPE, + Schedules.COLUMN_INPUT_ID, + Schedules.COLUMN_CHANNEL_ID, + Schedules.COLUMN_PROGRAM_ID, + Schedules.COLUMN_PROGRAM_TITLE, + Schedules.COLUMN_START_TIME_UTC_MILLIS, + Schedules.COLUMN_END_TIME_UTC_MILLIS, + Schedules.COLUMN_SEASON_NUMBER, + Schedules.COLUMN_EPISODE_NUMBER, + Schedules.COLUMN_EPISODE_TITLE, + Schedules.COLUMN_PROGRAM_DESCRIPTION, + Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, + Schedules.COLUMN_PROGRAM_POST_ART_URI, + Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, + Schedules.COLUMN_STATE, + Schedules.COLUMN_FAILED_REASON, + Schedules.COLUMN_SERIES_RECORDING_ID + }; - /** - * Creates {@link ScheduledRecording} object from the given {@link Cursor}. - */ + /** Creates {@link ScheduledRecording} object from the given {@link Cursor}. */ public static ScheduledRecording fromCursor(Cursor c) { int index = -1; return new Builder() @@ -379,6 +430,7 @@ public final class ScheduledRecording implements Parcelable { .setProgramPosterArtUri(c.getString(++index)) .setProgramThumbnailUri(c.getString(++index)) .setState(recordingState(c.getString(++index))) + .setFailedReason(recordingFailedReason(c.getString(++index))) .setSeriesRecordingId(c.getLong(++index)) .build(); } @@ -403,6 +455,7 @@ public final class ScheduledRecording implements Parcelable { values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri()); values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri()); values.put(Schedules.COLUMN_STATE, recordingState(r.getState())); + values.put(Schedules.COLUMN_FAILED_REASON, recordingFailedReason(r.getFailedReason())); values.put(Schedules.COLUMN_TYPE, recordingType(r.getType())); if (r.getSeriesRecordingId() != ID_NOT_SET) { values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId()); @@ -431,42 +484,40 @@ public final class ScheduledRecording implements Parcelable { .setProgramPosterArtUri(in.readString()) .setProgramThumbnailUri(in.readString()) .setState(in.readInt()) + .setFailedReason(recordingFailedReason(in.readString())) .setSeriesRecordingId(in.readLong()) .build(); } public static final Parcelable.Creator<ScheduledRecording> CREATOR = new Parcelable.Creator<ScheduledRecording>() { - @Override - public ScheduledRecording createFromParcel(Parcel in) { - return ScheduledRecording.fromParcel(in); - } - - @Override - public ScheduledRecording[] newArray(int size) { - return new ScheduledRecording[size]; - } - }; - - /** - * The ID internal to Live TV - */ + @Override + public ScheduledRecording createFromParcel(Parcel in) { + return ScheduledRecording.fromParcel(in); + } + + @Override + public ScheduledRecording[] newArray(int size) { + return new ScheduledRecording[size]; + } + }; + + /** The ID internal to Live TV */ private long mId; /** * The priority of this recording. * - * <p> The highest number is recorded first. If there is a tie in priority then the higher id + * <p>The highest number is recorded first. If there is a tie in priority then the higher id * wins. */ private final long mPriority; private final String mInputId; private final long mChannelId; - /** - * Optional id of the associated program. - */ + /** Optional id of the associated program. */ private final long mProgramId; + private final String mProgramTitle; private final long mStartTimeMs; @@ -480,12 +531,30 @@ public final class ScheduledRecording implements Parcelable { private final String mProgramThumbnailUri; @RecordingState private final int mState; private final long mSeriesRecordingId; - - private ScheduledRecording(long id, long priority, String inputId, long channelId, long programId, - String programTitle, @RecordingType int type, long startTime, long endTime, - String seasonNumber, String episodeNumber, String episodeTitle, - String programDescription, String programLongDescription, String programPosterArtUri, - String programThumbnailUri, @RecordingState int state, long seriesRecordingId) { + private final Long mRecordedProgramId; + private final Integer mFailedReason; + + private ScheduledRecording( + long id, + long priority, + String inputId, + long channelId, + long programId, + String programTitle, + @RecordingType int type, + long startTime, + long endTime, + String seasonNumber, + String episodeNumber, + String episodeTitle, + String programDescription, + String programLongDescription, + String programPosterArtUri, + String programThumbnailUri, + @RecordingState int state, + long seriesRecordingId, + Long recordedProgramId, + Integer failedReason) { mId = id; mPriority = priority; mInputId = inputId; @@ -504,139 +573,122 @@ public final class ScheduledRecording implements Parcelable { mProgramThumbnailUri = programThumbnailUri; mState = state; mSeriesRecordingId = seriesRecordingId; + mRecordedProgramId = recordedProgramId; + mFailedReason = failedReason; } /** - * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and - * {@link #TYPE_TIMED}. + * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and {@link + * #TYPE_TIMED}. */ @RecordingType public int getType() { return mType; } - /** - * Returns schedules' input id. - */ + /** Returns schedules' input id. */ public String getInputId() { return mInputId; } - /** - * Returns recorded {@link Channel}. - */ + /** Returns recorded {@link Channel}. */ public long getChannelId() { return mChannelId; } - /** - * Return the optional program id - */ + /** Return the optional program id */ public long getProgramId() { return mProgramId; } - /** - * Return the optional program Title - */ + /** Return the optional program Title */ public String getProgramTitle() { return mProgramTitle; } - /** - * Returns started time. - */ + /** Returns started time. */ public long getStartTimeMs() { return mStartTimeMs; } - /** - * Returns ended time. - */ + /** Returns ended time. */ public long getEndTimeMs() { return mEndTimeMs; } - /** - * Returns the season number. - */ + /** Returns the season number. */ public String getSeasonNumber() { return mSeasonNumber; } - /** - * Returns the episode number. - */ + /** Returns the episode number. */ public String getEpisodeNumber() { return mEpisodeNumber; } - /** - * Returns the episode title. - */ + /** Returns the episode title. */ public String getEpisodeTitle() { return mEpisodeTitle; } - /** - * Returns the description of program. - */ + /** Returns the description of program. */ public String getProgramDescription() { return mProgramDescription; } - /** - * Returns the long description of program. - */ + /** Returns the long description of program. */ public String getProgramLongDescription() { return mProgramLongDescription; } - /** - * Returns the poster uri of program. - */ + /** Returns the poster uri of program. */ public String getProgramPosterArtUri() { return mProgramPosterArtUri; } - /** - * Returns the thumb nail uri of program. - */ + /** Returns the thumb nail uri of program. */ public String getProgramThumbnailUri() { return mProgramThumbnailUri; } - /** - * Returns duration. - */ + /** Returns duration. */ public long getDuration() { return mEndTimeMs - mStartTimeMs; } /** - * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED}, - * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, - * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and - * {@link #STATE_RECORDING_DELETED}. + * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED}, {@link + * #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, {@link + * #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and {@link + * #STATE_RECORDING_DELETED}. */ - @RecordingState public int getState() { + @RecordingState + public int getState() { return mState; } - /** - * Returns the ID of the {@link SeriesRecording} including this schedule. - */ + /** Returns the ID of the {@link SeriesRecording} including this schedule. */ public long getSeriesRecordingId() { return mSeriesRecordingId; } + /** Returns the ID of the corresponding {@link RecordedProgram}. */ + @Nullable + public Long getRecordedProgramId() { + return mRecordedProgramId; + } + + /** Returns the failed reason of the {@link ScheduledRecording}. */ + @Nullable @RecordingFailedReason + public Integer getFailedReason() { + return mFailedReason; + } + public long getId() { return mId; } - /** - * Sets the ID; - */ + /** Sets the ID; */ public void setId(long id) { mId = id; } @@ -645,21 +697,23 @@ public final class ScheduledRecording implements Parcelable { return mPriority; } - /** - * Returns season number, episode number and episode title for display. - */ + /** Returns season number, episode number and episode title for display. */ public String getEpisodeDisplayTitle(Context context) { if (!TextUtils.isEmpty(mEpisodeNumber)) { String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; if (TextUtils.equals(mSeasonNumber, "0")) { // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_title_format_no_season_number), - mEpisodeNumber, episodeTitle); + return String.format( + context.getResources() + .getString(R.string.display_episode_title_format_no_season_number), + mEpisodeNumber, + episodeTitle); } else { - return String.format(context.getResources().getString( - R.string.display_episode_title_format), - mSeasonNumber, mEpisodeNumber, episodeTitle); + return String.format( + context.getResources().getString(R.string.display_episode_title_format), + mSeasonNumber, + mEpisodeNumber, + episodeTitle); } } return mEpisodeTitle; @@ -673,15 +727,14 @@ public final class ScheduledRecording implements Parcelable { if (!TextUtils.isEmpty(mProgramTitle)) { return mProgramTitle; } - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(mChannelId); - return channel != null ? channel.getDisplayName() + Channel channel = + TvSingletons.getSingletons(context).getChannelDataManager().getChannel(mChannelId); + return channel != null + ? channel.getDisplayName() : context.getString(R.string.no_program_information); } - /** - * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. - */ + /** Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. */ private static @RecordingType int recordingType(String type) { switch (type) { case Schedules.TYPE_TIMED: @@ -689,14 +742,12 @@ public final class ScheduledRecording implements Parcelable { case Schedules.TYPE_PROGRAM: return TYPE_PROGRAM; default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type); return TYPE_TIMED; } } - /** - * Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}. - */ + /** Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}. */ private static String recordingType(@RecordingType int type) { switch (type) { case TYPE_TIMED: @@ -704,14 +755,14 @@ public final class ScheduledRecording implements Parcelable { case TYPE_PROGRAM: return Schedules.TYPE_PROGRAM; default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type); return Schedules.TYPE_TIMED; } } /** - * Converts a string to a @RecordingState int, defaulting to - * {@link #STATE_RECORDING_NOT_STARTED}. + * Converts a string to a @RecordingState int, defaulting to {@link + * #STATE_RECORDING_NOT_STARTED}. */ private static @RecordingState int recordingState(String state) { switch (state) { @@ -730,14 +781,14 @@ public final class ScheduledRecording implements Parcelable { case Schedules.STATE_RECORDING_CANCELED: return STATE_RECORDING_CANCELED; default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state); return STATE_RECORDING_NOT_STARTED; } } /** - * Converts a @RecordingState int to string, defaulting to - * {@link Schedules#STATE_RECORDING_NOT_STARTED}. + * Converts a @RecordingState int to string, defaulting to {@link + * Schedules#STATE_RECORDING_NOT_STARTED}. */ private static String recordingState(@RecordingState int state) { switch (state) { @@ -756,46 +807,138 @@ public final class ScheduledRecording implements Parcelable { case STATE_RECORDING_CANCELED: return Schedules.STATE_RECORDING_CANCELED; default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state); return Schedules.STATE_RECORDING_NOT_STARTED; } } /** - * Checks if the {@code period} overlaps with the recording time. + * Converts a string to a failed reason integer, defaulting to {@link + * #FAILED_REASON_OTHER}. */ - public boolean isOverLapping(Range<Long> period) { - return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); + private static Integer recordingFailedReason(String reason) { + if (TextUtils.isEmpty(reason)) { + return null; + } + switch (reason) { + case Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; + case Schedules.FAILED_REASON_NOT_FINISHED: + return FAILED_REASON_NOT_FINISHED; + case Schedules.FAILED_REASON_SCHEDULER_STOPPED: + return FAILED_REASON_SCHEDULER_STOPPED; + case Schedules.FAILED_REASON_INVALID_CHANNEL: + return FAILED_REASON_INVALID_CHANNEL; + case Schedules.FAILED_REASON_MESSAGE_NOT_SENT: + return FAILED_REASON_MESSAGE_NOT_SENT; + case Schedules.FAILED_REASON_CONNECTION_FAILED: + return FAILED_REASON_CONNECTION_FAILED; + case Schedules.FAILED_REASON_RESOURCE_BUSY: + return FAILED_REASON_RESOURCE_BUSY; + case Schedules.FAILED_REASON_INPUT_UNAVAILABLE: + return FAILED_REASON_INPUT_UNAVAILABLE; + case Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return FAILED_REASON_INPUT_DVR_UNSUPPORTED; + case Schedules.FAILED_REASON_INSUFFICIENT_SPACE: + return FAILED_REASON_INSUFFICIENT_SPACE; + case Schedules.FAILED_REASON_OTHER: + default: + return FAILED_REASON_OTHER; + } } /** - * Checks if the {@code schedule} overlaps with this schedule. + * Converts a failed reason integer to string, defaulting to {@link + * Schedules#FAILED_REASON_OTHER}. */ + private static String recordingFailedReason(Integer reason) { + if (reason == null) { + return null; + } + switch (reason) { + case FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; + case FAILED_REASON_NOT_FINISHED: + return Schedules.FAILED_REASON_NOT_FINISHED; + case FAILED_REASON_SCHEDULER_STOPPED: + return Schedules.FAILED_REASON_SCHEDULER_STOPPED; + case FAILED_REASON_INVALID_CHANNEL: + return Schedules.FAILED_REASON_INVALID_CHANNEL; + case FAILED_REASON_MESSAGE_NOT_SENT: + return Schedules.FAILED_REASON_MESSAGE_NOT_SENT; + case FAILED_REASON_CONNECTION_FAILED: + return Schedules.FAILED_REASON_CONNECTION_FAILED; + case FAILED_REASON_RESOURCE_BUSY: + return Schedules.FAILED_REASON_RESOURCE_BUSY; + case FAILED_REASON_INPUT_UNAVAILABLE: + return Schedules.FAILED_REASON_INPUT_UNAVAILABLE; + case FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED; + case FAILED_REASON_INSUFFICIENT_SPACE: + return Schedules.FAILED_REASON_INSUFFICIENT_SPACE; + case FAILED_REASON_OTHER: // fall through + default: + return Schedules.FAILED_REASON_OTHER; + } + } + + /** Checks if the {@code period} overlaps with the recording time. */ + public boolean isOverLapping(Range<Long> period) { + return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); + } + + /** Checks if the {@code schedule} overlaps with this schedule. */ public boolean isOverLapping(ScheduledRecording schedule) { return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs(); } @Override public String toString() { - return "ScheduledRecording[" + mId + return "ScheduledRecording[" + + mId + "]" - + "(inputId=" + mInputId - + ",channelId=" + mChannelId - + ",programId=" + mProgramId - + ",programTitle=" + mProgramTitle - + ",type=" + mType - + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")" - + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")" - + ",seasonNumber=" + mSeasonNumber - + ",episodeNumber=" + mEpisodeNumber - + ",episodeTitle=" + mEpisodeTitle - + ",programDescription=" + mProgramDescription - + ",programLongDescription=" + mProgramLongDescription - + ",programPosterArtUri=" + mProgramPosterArtUri - + ",programThumbnailUri=" + mProgramThumbnailUri - + ",state=" + mState - + ",priority=" + mPriority - + ",seriesRecordingId=" + mSeriesRecordingId + + "(inputId=" + + mInputId + + ",channelId=" + + mChannelId + + ",programId=" + + mProgramId + + ",programTitle=" + + mProgramTitle + + ",type=" + + mType + + ",startTime=" + + CommonUtils.toIsoDateTimeString(mStartTimeMs) + + "(" + + mStartTimeMs + + ")" + + ",endTime=" + + CommonUtils.toIsoDateTimeString(mEndTimeMs) + + "(" + + mEndTimeMs + + ")" + + ",seasonNumber=" + + mSeasonNumber + + ",episodeNumber=" + + mEpisodeNumber + + ",episodeTitle=" + + mEpisodeTitle + + ",programDescription=" + + mProgramDescription + + ",programLongDescription=" + + mProgramLongDescription + + ",programPosterArtUri=" + + mProgramPosterArtUri + + ",programThumbnailUri=" + + mProgramThumbnailUri + + ",state=" + + mState + + ",failedReason=" + + mFailedReason + + ",priority=" + + mPriority + + ",seriesRecordingId=" + + mSeriesRecordingId + ")"; } @@ -823,23 +966,25 @@ public final class ScheduledRecording implements Parcelable { out.writeString(mProgramPosterArtUri); out.writeString(mProgramThumbnailUri); out.writeInt(mState); + out.writeString(recordingFailedReason(mFailedReason)); out.writeLong(mSeriesRecordingId); } - /** - * Returns {@code true} if the recording is not started yet, otherwise @{code false}. - */ + /** Returns {@code true} if the recording is not started yet, otherwise @{code false}. */ public boolean isNotStarted() { return mState == STATE_RECORDING_NOT_STARTED; } - /** - * Returns {@code true} if the recording is in progress, otherwise @{code false}. - */ + /** Returns {@code true} if the recording is in progress, otherwise @{code false}. */ public boolean isInProgress() { return mState == STATE_RECORDING_IN_PROGRESS; } + /** Returns {@code true} if the recording is finished, otherwise @{code false}. */ + public boolean isFinished() { + return mState == STATE_RECORDING_FINISHED; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof ScheduledRecording)) { @@ -862,20 +1007,34 @@ public final class ScheduledRecording implements Parcelable { && Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri()) && Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri()) && mState == r.mState + && Objects.equals(mFailedReason, r.mFailedReason) && mSeriesRecordingId == r.mSeriesRecordingId; } @Override public int hashCode() { - return Objects.hash(mId, mPriority, mChannelId, mProgramId, mProgramTitle, mType, - mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, mEpisodeTitle, - mProgramDescription, mProgramLongDescription, mProgramPosterArtUri, - mProgramThumbnailUri, mState, mSeriesRecordingId); + return Objects.hash( + mId, + mPriority, + mChannelId, + mProgramId, + mProgramTitle, + mType, + mStartTimeMs, + mEndTimeMs, + mSeasonNumber, + mEpisodeNumber, + mEpisodeTitle, + mProgramDescription, + mProgramLongDescription, + mProgramPosterArtUri, + mProgramThumbnailUri, + mState, + mFailedReason, + mSeriesRecordingId); } - /** - * Returns an array containing all of the elements in the list. - */ + /** Returns an array containing all of the elements in the list. */ public static ScheduledRecording[] toArray(Collection<ScheduledRecording> schedules) { return schedules.toArray(new ScheduledRecording[schedules.size()]); } diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java index 89533dbb..c697451a 100644 --- a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java +++ b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java @@ -17,20 +17,15 @@ package com.android.tv.dvr.data; import android.text.TextUtils; - import java.util.Objects; -/** - * A plain java object which includes the season/episode number for the series recording. - */ +/** A plain java object which includes the season/episode number for the series recording. */ public class SeasonEpisodeNumber { public final long seriesRecordingId; public final String seasonNumber; public final String episodeNumber; - /** - * Creates a new Builder with the values set from an existing {@link ScheduledRecording}. - */ + /** Creates a new Builder with the values set from an existing {@link ScheduledRecording}. */ public SeasonEpisodeNumber(ScheduledRecording r) { this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); } @@ -47,7 +42,8 @@ public class SeasonEpisodeNumber { return true; } if (!(o instanceof SeasonEpisodeNumber) - || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) { + || TextUtils.isEmpty(seasonNumber) + || TextUtils.isEmpty(episodeNumber)) { return false; } SeasonEpisodeNumber that = (SeasonEpisodeNumber) o; @@ -63,10 +59,13 @@ public class SeasonEpisodeNumber { @Override public String toString() { - return "SeasonEpisodeNumber{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; + return "SeasonEpisodeNumber{" + + "seriesRecordingId=" + + seriesRecordingId + + ", seasonNumber='" + + seasonNumber + + ", episodeNumber=" + + episodeNumber + + '}'; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/data/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java index a0dec4a4..aa622c3d 100644 --- a/src/com/android/tv/dvr/data/SeriesInfo.java +++ b/src/com/android/tv/dvr/data/SeriesInfo.java @@ -16,9 +16,7 @@ package com.android.tv.dvr.data; -/** - * Series information. - */ +/** Series information. */ public class SeriesInfo { private final String mId; private final String mTitle; @@ -28,8 +26,14 @@ public class SeriesInfo { private final String mPosterUri; private final String mPhotoUri; - public SeriesInfo(String id, String title, String description, String longDescription, - int[] canonicalGenreIds, String posterUri, String photoUri) { + public SeriesInfo( + String id, + String title, + String description, + String longDescription, + int[] canonicalGenreIds, + String posterUri, + String photoUri) { this.mId = id; this.mTitle = title; this.mDescription = description; @@ -39,37 +43,37 @@ public class SeriesInfo { this.mPhotoUri = photoUri; } - /** Returns the ID. **/ + /** Returns the ID. * */ public String getId() { return mId; } - /** Returns the title. **/ + /** Returns the title. * */ public String getTitle() { return mTitle; } - /** Returns the description. **/ + /** Returns the description. * */ public String getDescription() { return mDescription; } - /** Returns the description. **/ + /** Returns the description. * */ public String getLongDescription() { return mLongDescription; } - /** Returns the canonical genre IDs. **/ + /** Returns the canonical genre IDs. * */ public int[] getCanonicalGenreIds() { return mCanonicalGenreIds; } - /** Returns the poster URI. **/ + /** Returns the poster URI. * */ public String getPosterUri() { return mPosterUri; } - /** Returns the photo URI. **/ + /** Returns the photo URI. * */ public String getPhotoUri() { return mPhotoUri; } diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java index 822e7320..96b3425a 100644 --- a/src/com/android/tv/dvr/data/SeriesRecording.java +++ b/src/com/android/tv/dvr/data/SeriesRecording.java @@ -21,15 +21,12 @@ import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; - import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; @@ -40,68 +37,55 @@ import java.util.Objects; /** * Schedules the recording of a Series of Programs. * - * <p> - * Contains the data needed to create new ScheduleRecordings as the programs become available in + * <p>Contains the data needed to create new ScheduleRecordings as the programs become available in * the EPG. */ public class SeriesRecording implements Parcelable { - /** - * Indicates that the ID is not assigned yet. - */ + /** Indicates that the ID is not assigned yet. */ public static final long ID_NOT_SET = 0; - /** - * The default priority of this recording. - */ + /** The default priority of this recording. */ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}) + @IntDef( + flag = true, + value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL} + ) public @interface ChannelOption {} - /** - * An option which indicates that the episodes in one channel are recorded. - */ + /** An option which indicates that the episodes in one channel are recorded. */ public static final int OPTION_CHANNEL_ONE = 0; - /** - * An option which indicates that the episodes in all the channels are recorded. - */ + /** An option which indicates that the episodes in all the channels are recorded. */ public static final int OPTION_CHANNEL_ALL = 1; @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) + @IntDef( + flag = true, + value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED} + ) public @interface SeriesState {} - /** - * The state indicates that the series recording is a normal one. - */ + /** The state indicates that the series recording is a normal one. */ public static final int STATE_SERIES_NORMAL = 0; - /** - * The state indicates that the series recording is stopped. - */ + /** The state indicates that the series recording is stopped. */ public static final int STATE_SERIES_STOPPED = 1; - /** - * Compare priority in descending order. - */ + /** Compare priority in descending order. */ public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR = new Comparator<SeriesRecording>() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - int value = Long.compare(rhs.mPriority, lhs.mPriority); - if (value == 0) { - // New recording has the higher priority. - value = Long.compare(rhs.mId, lhs.mId); - } - return value; - } - }; + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + int value = Long.compare(rhs.mPriority, lhs.mPriority); + if (value == 0) { + // New recording has the higher priority. + value = Long.compare(rhs.mId, lhs.mId); + } + return value; + } + }; - /** - * Compare ID in ascending order. - */ + /** Compare ID in ascending order. */ public static final Comparator<SeriesRecording> ID_COMPARATOR = new Comparator<SeriesRecording>() { @Override @@ -126,9 +110,7 @@ public class SeriesRecording implements Parcelable { .setPhotoUri(p.getThumbnailUri()); } - /** - * Creates a new Builder with the values set from an existing {@link SeriesRecording}. - */ + /** Creates a new Builder with the values set from an existing {@link SeriesRecording}. */ public static Builder buildFrom(SeriesRecording r) { return new Builder() .setId(r.mId) @@ -149,30 +131,28 @@ public class SeriesRecording implements Parcelable { } /** - * Use this projection if you want to create {@link SeriesRecording} object using - * {@link #fromCursor}. + * Use this projection if you want to create {@link SeriesRecording} object using {@link + * #fromCursor}. */ public static final String[] PROJECTION = { - // Columns must match what is read in fromCursor() - SeriesRecordings._ID, - SeriesRecordings.COLUMN_INPUT_ID, - SeriesRecordings.COLUMN_CHANNEL_ID, - SeriesRecordings.COLUMN_PRIORITY, - SeriesRecordings.COLUMN_TITLE, - SeriesRecordings.COLUMN_SHORT_DESCRIPTION, - SeriesRecordings.COLUMN_LONG_DESCRIPTION, - SeriesRecordings.COLUMN_SERIES_ID, - SeriesRecordings.COLUMN_START_FROM_EPISODE, - SeriesRecordings.COLUMN_START_FROM_SEASON, - SeriesRecordings.COLUMN_CHANNEL_OPTION, - SeriesRecordings.COLUMN_CANONICAL_GENRE, - SeriesRecordings.COLUMN_POSTER_URI, - SeriesRecordings.COLUMN_PHOTO_URI, - SeriesRecordings.COLUMN_STATE + // Columns must match what is read in fromCursor() + SeriesRecordings._ID, + SeriesRecordings.COLUMN_INPUT_ID, + SeriesRecordings.COLUMN_CHANNEL_ID, + SeriesRecordings.COLUMN_PRIORITY, + SeriesRecordings.COLUMN_TITLE, + SeriesRecordings.COLUMN_SHORT_DESCRIPTION, + SeriesRecordings.COLUMN_LONG_DESCRIPTION, + SeriesRecordings.COLUMN_SERIES_ID, + SeriesRecordings.COLUMN_START_FROM_EPISODE, + SeriesRecordings.COLUMN_START_FROM_SEASON, + SeriesRecordings.COLUMN_CHANNEL_OPTION, + SeriesRecordings.COLUMN_CANONICAL_GENRE, + SeriesRecordings.COLUMN_POSTER_URI, + SeriesRecordings.COLUMN_PHOTO_URI, + SeriesRecordings.COLUMN_STATE }; - /** - * Creates {@link SeriesRecording} object from the given {@link Cursor}. - */ + /** Creates {@link SeriesRecording} object from the given {@link Cursor}. */ public static SeriesRecording fromCursor(Cursor c) { int index = -1; return new Builder() @@ -195,8 +175,8 @@ public class SeriesRecording implements Parcelable { } /** - * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings} - * and the values from {@code r}. + * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings} and + * the values from {@code r}. */ public static ContentValues toContentValues(SeriesRecording r) { ContentValues values = new ContentValues(); @@ -214,9 +194,9 @@ public class SeriesRecording implements Parcelable { values.put(SeriesRecordings.COLUMN_SERIES_ID, r.getSeriesId()); values.put(SeriesRecordings.COLUMN_START_FROM_EPISODE, r.getStartFromEpisode()); values.put(SeriesRecordings.COLUMN_START_FROM_SEASON, r.getStartFromSeason()); - values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION, - channelOption(r.getChannelOption())); - values.put(SeriesRecordings.COLUMN_CANONICAL_GENRE, + values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION, channelOption(r.getChannelOption())); + values.put( + SeriesRecordings.COLUMN_CANONICAL_GENRE, Utils.getCanonicalGenre(r.getCanonicalGenreIds())); values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri()); values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri()); @@ -234,7 +214,8 @@ public class SeriesRecording implements Parcelable { return SeriesRecordings.OPTION_CHANNEL_ONE; } - @ChannelOption private static int channelOption(String option) { + @ChannelOption + private static int channelOption(String option) { switch (option) { case SeriesRecordings.OPTION_CHANNEL_ONE: return OPTION_CHANNEL_ONE; @@ -254,7 +235,8 @@ public class SeriesRecording implements Parcelable { return SeriesRecordings.STATE_SERIES_NORMAL; } - @SeriesState private static int seriesRecordingState(String state) { + @SeriesState + private static int seriesRecordingState(String state) { switch (state) { case SeriesRecordings.STATE_SERIES_NORMAL: return STATE_SERIES_NORMAL; @@ -264,9 +246,7 @@ public class SeriesRecording implements Parcelable { return STATE_SERIES_NORMAL; } - /** - * Builder for {@link SeriesRecording}. - */ + /** Builder for {@link SeriesRecording}. */ public static class Builder { private long mId = ID_NOT_SET; private long mPriority = DvrScheduleManager.DEFAULT_SERIES_PRIORITY; @@ -284,141 +264,120 @@ public class SeriesRecording implements Parcelable { private String mPhotoUri; private int mState = SeriesRecording.STATE_SERIES_NORMAL; - /** - * @see #getId() - */ + /** @see #getId() */ public Builder setId(long id) { mId = id; return this; } - /** - * @see #getPriority() () - */ + /** @see #getPriority() () */ public Builder setPriority(long priority) { mPriority = priority; return this; } - /** - * @see #getTitle() - */ + /** @see #getTitle() */ public Builder setTitle(String title) { mTitle = title; return this; } - /** - * @see #getDescription() - */ + /** @see #getDescription() */ public Builder setDescription(String description) { mDescription = description; return this; } - /** - * @see #getLongDescription() - */ + /** @see #getLongDescription() */ public Builder setLongDescription(String longDescription) { mLongDescription = longDescription; return this; } - /** - * @see #getInputId() - */ + /** @see #getInputId() */ public Builder setInputId(String inputId) { mInputId = inputId; return this; } - /** - * @see #getChannelId() - */ + /** @see #getChannelId() */ public Builder setChannelId(long channelId) { mChannelId = channelId; return this; } - /** - * @see #getSeriesId() - */ + /** @see #getSeriesId() */ public Builder setSeriesId(String seriesId) { mSeriesId = seriesId; return this; } - /** - * @see #getStartFromSeason() - */ + /** @see #getStartFromSeason() */ public Builder setStartFromSeason(int startFromSeason) { mStartFromSeason = startFromSeason; return this; } - /** - * @see #getChannelOption() - */ + /** @see #getChannelOption() */ public Builder setChannelOption(@ChannelOption int option) { mChannelOption = option; return this; } - /** - * @see #getStartFromEpisode() - */ + /** @see #getStartFromEpisode() */ public Builder setStartFromEpisode(int startFromEpisode) { mStartFromEpisode = startFromEpisode; return this; } - /** - * @see #getCanonicalGenreIds() - */ + /** @see #getCanonicalGenreIds() */ public Builder setCanonicalGenreIds(String genres) { mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres); return this; } - /** - * @see #getCanonicalGenreIds() - */ + /** @see #getCanonicalGenreIds() */ public Builder setCanonicalGenreIds(int[] canonicalGenreIds) { mCanonicalGenreIds = canonicalGenreIds; return this; } - /** - * @see #getPosterUri() - */ + /** @see #getPosterUri() */ public Builder setPosterUri(String posterUri) { mPosterUri = posterUri; return this; } - /** - * @see #getPhotoUri() - */ + /** @see #getPhotoUri() */ public Builder setPhotoUri(String photoUri) { mPhotoUri = photoUri; return this; } - /** - * @see #getState() - */ + /** @see #getState() */ public Builder setState(@SeriesState int state) { mState = state; return this; } - /** - * Creates a new {@link SeriesRecording}. - */ + /** Creates a new {@link SeriesRecording}. */ public SeriesRecording build() { - return new SeriesRecording(mId, mPriority, mTitle, mDescription, mLongDescription, - mInputId, mChannelId, mSeriesId, mStartFromSeason, mStartFromEpisode, - mChannelOption, mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); + return new SeriesRecording( + mId, + mPriority, + mTitle, + mDescription, + mLongDescription, + mInputId, + mChannelId, + mSeriesId, + mStartFromSeason, + mStartFromEpisode, + mChannelOption, + mCanonicalGenreIds, + mPosterUri, + mPhotoUri, + mState); } } @@ -444,16 +403,16 @@ public class SeriesRecording implements Parcelable { public static final Parcelable.Creator<SeriesRecording> CREATOR = new Parcelable.Creator<SeriesRecording>() { - @Override - public SeriesRecording createFromParcel(Parcel in) { - return SeriesRecording.fromParcel(in); - } + @Override + public SeriesRecording createFromParcel(Parcel in) { + return SeriesRecording.fromParcel(in); + } - @Override - public SeriesRecording[] newArray(int size) { - return new SeriesRecording[size]; - } - }; + @Override + public SeriesRecording[] newArray(int size) { + return new SeriesRecording[size]; + } + }; private long mId; private final long mPriority; @@ -471,9 +430,7 @@ public class SeriesRecording implements Parcelable { private final String mPhotoUri; @SeriesState private int mState; - /** - * The input id of this SeriesRecording. - */ + /** The input id of this SeriesRecording. */ public String getInputId() { return mInputId; } @@ -485,16 +442,12 @@ public class SeriesRecording implements Parcelable { return mChannelId; } - /** - * The id of this SeriesRecording. - */ + /** The id of this SeriesRecording. */ public long getId() { return mId; } - /** - * Sets the ID. - */ + /** Sets the ID. */ public void setId(long id) { mId = id; } @@ -502,30 +455,24 @@ public class SeriesRecording implements Parcelable { /** * The priority of this recording. * - * <p> The highest number is recorded first. If there is a tie in mPriority then the higher mId + * <p>The highest number is recorded first. If there is a tie in mPriority then the higher mId * wins. */ public long getPriority() { return mPriority; } - /** - * The series title. - */ + /** The series title. */ public String getTitle() { return mTitle; } - /** - * The series description. - */ + /** The series description. */ public String getDescription() { return mDescription; } - /** - * The long series description. - */ + /** The long series description. */ public String getLongDescription() { return mLongDescription; } @@ -555,44 +502,34 @@ public class SeriesRecording implements Parcelable { return mStartFromSeason; } - /** - * Returns the channel recording option. - */ - @ChannelOption public int getChannelOption() { + /** Returns the channel recording option. */ + @ChannelOption + public int getChannelOption() { return mChannelOption; } - /** - * Returns the canonical genre ID's. - */ + /** Returns the canonical genre ID's. */ public int[] getCanonicalGenreIds() { return mCanonicalGenreIds; } - /** - * Returns the poster URI. - */ + /** Returns the poster URI. */ public String getPosterUri() { return mPosterUri; } - /** - * Returns the photo URI. - */ + /** Returns the photo URI. */ public String getPhotoUri() { return mPhotoUri; } - /** - * Returns the state of series recording. - */ - @SeriesState public int getState() { + /** Returns the state of series recording. */ + @SeriesState + public int getState() { return mState; } - /** - * Checks whether the series recording is stopped or not. - */ + /** Checks whether the series recording is stopped or not. */ public boolean isStopped() { return mState == STATE_SERIES_STOPPED; } @@ -620,35 +557,77 @@ public class SeriesRecording implements Parcelable { @Override public int hashCode() { - return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId, - mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption, - mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); + return Objects.hash( + mPriority, + mChannelId, + mStartFromSeason, + mStartFromEpisode, + mId, + mTitle, + mDescription, + mLongDescription, + mSeriesId, + mChannelOption, + Arrays.hashCode(mCanonicalGenreIds), + mPosterUri, + mPhotoUri, + mState); } @Override public String toString() { - return "SeriesRecording{" + - "inputId=" + mInputId + - ", channelId=" + mChannelId + - ", id='" + mId + '\'' + - ", priority=" + mPriority + - ", title='" + mTitle + '\'' + - ", description='" + mDescription + '\'' + - ", longDescription='" + mLongDescription + '\'' + - ", startFromSeason=" + mStartFromSeason + - ", startFromEpisode=" + mStartFromEpisode + - ", channelOption=" + mChannelOption + - ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) + - ", posterUri=" + mPosterUri + - ", photoUri=" + mPhotoUri + - ", state=" + mState + - '}'; - } - - private SeriesRecording(long id, long priority, String title, String description, - String longDescription, String inputId, long channelId, String seriesId, - int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds, - String posterUri, String photoUri, int state) { + return "SeriesRecording{" + + "inputId=" + + mInputId + + ", channelId=" + + mChannelId + + ", id='" + + mId + + '\'' + + ", priority=" + + mPriority + + ", title='" + + mTitle + + '\'' + + ", description='" + + mDescription + + '\'' + + ", longDescription='" + + mLongDescription + + '\'' + + ", startFromSeason=" + + mStartFromSeason + + ", startFromEpisode=" + + mStartFromEpisode + + ", channelOption=" + + mChannelOption + + ", canonicalGenreIds=" + + Arrays.toString(mCanonicalGenreIds) + + ", posterUri=" + + mPosterUri + + ", photoUri=" + + mPhotoUri + + ", state=" + + mState + + '}'; + } + + private SeriesRecording( + long id, + long priority, + String title, + String description, + String longDescription, + String inputId, + long channelId, + String seriesId, + int startFromSeason, + int startFromEpisode, + int channelOption, + int[] canonicalGenreIds, + String posterUri, + String photoUri, + int state) { this.mId = id; this.mPriority = priority; this.mTitle = title; @@ -690,9 +669,7 @@ public class SeriesRecording implements Parcelable { out.writeInt(mState); } - /** - * Returns an array containing all of the elements in the list. - */ + /** Returns an array containing all of the elements in the list. */ public static SeriesRecording[] toArray(Collection<SeriesRecording> series) { return series.toArray(new SeriesRecording[series.size()]); } @@ -715,16 +692,16 @@ public class SeriesRecording implements Parcelable { long channelId = program.getChannelId(); String seasonNumber = program.getSeasonNumber(); String episodeNumber = program.getEpisodeNumber(); - if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE - && mChannelId != channelId)) { + if (!mSeriesId.equals(seriesId) + || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mChannelId != channelId)) { return false; } // Season number and episode number matches if // start_season_number < program_season_number // || (start_season_number == program_season_number // && start_episode_number <= program_episode_number). - if (mStartFromSeason == SeriesRecordings.THE_BEGINNING - || TextUtils.isEmpty(seasonNumber)) { + if (mStartFromSeason == SeriesRecordings.THE_BEGINNING || TextUtils.isEmpty(seasonNumber)) { return true; } else { int intSeasonNumber; diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index c5383d02..7d2af9c3 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -20,27 +20,23 @@ import android.content.Context; import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; - +import com.android.tv.common.concurrent.NamedThreadFactory; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; -import com.android.tv.util.NamedThreadFactory; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** - * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. - */ +/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */ public abstract class AsyncDvrDbTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> { - private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory( - AsyncDvrDbTask.class.getSimpleName()); - private static final ExecutorService DB_EXECUTOR = Executors - .newSingleThreadExecutor(THREAD_FACTORY); + private static final NamedThreadFactory THREAD_FACTORY = + new NamedThreadFactory(AsyncDvrDbTask.class.getSimpleName()); + private static final ExecutorService DB_EXECUTOR = + Executors.newSingleThreadExecutor(THREAD_FACTORY); private static DvrDatabaseHelper sDbHelper; @@ -57,9 +53,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> mContext = context; } - /** - * Execute the task on the {@link #DB_EXECUTOR} thread. - */ + /** Execute the task on the {@link #DB_EXECUTOR} thread. */ @SafeVarargs public final void executeOnDbThread(Params... params) { executeOnExecutor(DB_EXECUTOR, params); @@ -71,15 +65,11 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> return doInDvrBackground(params); } - /** - * Executes in the background after {@link #initializeDbHelper(Context)} - */ + /** Executes in the background after {@link #initializeDbHelper(Context)} */ @Nullable protected abstract Result doInDvrBackground(Params... params); - /** - * Inserts schedules. - */ + /** Inserts schedules. */ public static class AsyncAddScheduleTask extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { public AsyncAddScheduleTask(Context context) { @@ -93,9 +83,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Update schedules. - */ + /** Update schedules. */ public static class AsyncUpdateScheduleTask extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { public AsyncUpdateScheduleTask(Context context) { @@ -109,9 +97,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Delete schedules. - */ + /** Delete schedules. */ public static class AsyncDeleteScheduleTask extends AsyncDvrDbTask<ScheduledRecording, Void, Void> { public AsyncDeleteScheduleTask(Context context) { @@ -125,9 +111,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Returns all {@link ScheduledRecording}s. - */ + /** Returns all {@link ScheduledRecording}s. */ public abstract static class AsyncDvrQueryScheduleTask extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> { public AsyncDvrQueryScheduleTask(Context context) { @@ -150,9 +134,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Inserts series recordings. - */ + /** Inserts series recordings. */ public static class AsyncAddSeriesRecordingTask extends AsyncDvrDbTask<SeriesRecording, Void, Void> { public AsyncAddSeriesRecordingTask(Context context) { @@ -166,9 +148,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Update series recordings. - */ + /** Update series recordings. */ public static class AsyncUpdateSeriesRecordingTask extends AsyncDvrDbTask<SeriesRecording, Void, Void> { public AsyncUpdateSeriesRecordingTask(Context context) { @@ -182,9 +162,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Delete series recordings. - */ + /** Delete series recordings. */ public static class AsyncDeleteSeriesRecordingTask extends AsyncDvrDbTask<SeriesRecording, Void, Void> { public AsyncDeleteSeriesRecordingTask(Context context) { @@ -198,9 +176,7 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> } } - /** - * Returns all {@link SeriesRecording}s. - */ + /** Returns all {@link SeriesRecording}s. */ public abstract static class AsyncDvrQuerySeriesRecordingTask extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> { public AsyncDvrQuerySeriesRecordingTask(Context context) { @@ -214,8 +190,8 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result> return null; } List<SeriesRecording> scheduledRecordings = new ArrayList<>(); - try (Cursor c = sDbHelper.query(SeriesRecordings.TABLE_NAME, - SeriesRecording.PROJECTION)) { + try (Cursor c = + sDbHelper.query(SeriesRecordings.TABLE_NAME, SeriesRecording.PROJECTION)) { while (c.moveToNext() && !isCancelled()) { scheduledRecordings.add(SeriesRecording.fromCursor(c)); } diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java index f0aca18e..a5f2e2cd 100644 --- a/src/com/android/tv/dvr/provider/DvrContract.java +++ b/src/com/android/tv/dvr/provider/DvrContract.java @@ -55,11 +55,57 @@ public final class DvrContract { /** The recording marked as canceled. */ public static final String STATE_RECORDING_CANCELED = "STATE_RECORDING_CANCELED"; + /** The recording failed reason for other reasons */ + public static final String FAILED_REASON_OTHER = "FAILED_REASON_OTHER"; + + /** The recording failed because the program ended before recording started. */ + public static final String FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = + "FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED"; + + /** The recording failed because it was not finished successfully */ + public static final String FAILED_REASON_NOT_FINISHED = "FAILED_REASON_NOT_FINISHED"; + + /** The recording failed because the channel ID was invalid */ + public static final String FAILED_REASON_INVALID_CHANNEL = "FAILED_REASON_INVALID_CHANNEL"; + + /** The recording failed because the scheduler was stopped */ + public static final String FAILED_REASON_SCHEDULER_STOPPED + = "FAILED_REASON_SCHEDULER_STOPPED"; + + /** The recording failed because some messages were not sent to the message queue */ + public static final String FAILED_REASON_MESSAGE_NOT_SENT = + "FAILED_REASON_MESSAGE_NOT_SENT"; + + /** + * The recording failed because it was failed to establish a connection to the recording + * session for the corresponding TV input. + */ + public static final String FAILED_REASON_CONNECTION_FAILED = + "FAILED_REASON_CONNECTION_FAILED"; + + /** + * The recording failed because a required recording resource was not able to be + * allocated. + */ + public static final String FAILED_REASON_RESOURCE_BUSY = "FAILED_REASON_RESOURCE_BUSY"; + + /** The recording failed because the input was not available */ + public static final String FAILED_REASON_INPUT_UNAVAILABLE = + "FAILED_REASON_INPUT_UNAVAILABLE"; + + /** The recording failed because the input doesn't support recording */ + public static final String FAILED_REASON_INPUT_DVR_UNSUPPORTED = + "FAILED_REASON_INPUT_DVR_UNSUPPORTED"; + + /** The recording failed because the space was not sufficient */ + public static final String FAILED_REASON_INSUFFICIENT_SPACE = + "FAILED_REASON_INSUFFICIENT_SPACE"; + /** * The priority of this recording. * - * <p> The lowest number is recorded first. If there is a tie in priority then the lower id - * wins. Defaults to {@value Long#MAX_VALUE} + * <p>The lowest number is recorded first. If there is a tie in priority then the lower id + * wins. Defaults to {@value Long#MAX_VALUE} * * <p>Type: INTEGER (long) */ @@ -68,8 +114,8 @@ public final class DvrContract { /** * The type of this recording. * - * <p>This value should be one of the followings: {@link #TYPE_PROGRAM} and - * {@link #TYPE_TIMED}. + * <p>This value should be one of the followings: {@link #TYPE_PROGRAM} and {@link + * #TYPE_TIMED}. * * <p>This is a required field. * @@ -184,9 +230,9 @@ public final class DvrContract { * The state of this recording. * * <p>This value should be one of the followings: {@link #STATE_RECORDING_NOT_STARTED}, - * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, - * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and - * {@link #STATE_RECORDING_DELETED}. + * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, {@link + * #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and {@link + * #STATE_RECORDING_DELETED}. * * <p>This is a required field. * @@ -195,13 +241,20 @@ public final class DvrContract { public static final String COLUMN_STATE = "state"; /** + * The reason of failure of this recording if it's failed. + * + * <p>Type: TEXT + */ + public static final String COLUMN_FAILED_REASON = "failed_reason"; + + /** * The ID of the parent series recording. * * <p>Type: INTEGER (long) */ public static final String COLUMN_SERIES_RECORDING_ID = "series_recording_id"; - private Schedules() { } + private Schedules() {} } /** Column definition for Recording table. */ @@ -210,8 +263,8 @@ public final class DvrContract { public static final String TABLE_NAME = "series_recording"; /** - * This value is used for {@link #COLUMN_START_FROM_SEASON} and - * {@link #COLUMN_START_FROM_EPISODE} to mean record all seasons or episodes. + * This value is used for {@link #COLUMN_START_FROM_SEASON} and {@link + * #COLUMN_START_FROM_EPISODE} to mean record all seasons or episodes. */ public static final int THE_BEGINNING = -1; @@ -227,21 +280,17 @@ public final class DvrContract { */ public static final String OPTION_CHANNEL_ALL = "OPTION_CHANNEL_ALL"; - /** - * The state indicates that it is a normal one. - */ + /** The state indicates that it is a normal one. */ public static final String STATE_SERIES_NORMAL = "STATE_SERIES_NORMAL"; - /** - * The state indicates that it is stopped. - */ + /** The state indicates that it is stopped. */ public static final String STATE_SERIES_STOPPED = "STATE_SERIES_STOPPED"; /** * The priority of this recording. * - * <p> The lowest number is recorded first. If there is a tie in priority then the lower id - * wins. Defaults to {@value Long#MAX_VALUE} + * <p>The lowest number is recorded first. If there is a tie in priority then the lower id + * wins. Defaults to {@value Long#MAX_VALUE} * * <p>Type: INTEGER (long) */ @@ -266,7 +315,7 @@ public final class DvrContract { public static final String COLUMN_CHANNEL_ID = "channel_id"; /** - * The ID of the associated series to record. + * The ID of the associated series to record. * * <p>The id is an opaque but stable string. * @@ -300,8 +349,8 @@ public final class DvrContract { public static final String COLUMN_LONG_DESCRIPTION = "long_description"; /** - * The number of the earliest season to record. The - * value {@link #THE_BEGINNING} means record all seasons. + * The number of the earliest season to record. The value {@link #THE_BEGINNING} means + * record all seasons. * * <p>Default value is {@value #THE_BEGINNING} {@link #THE_BEGINNING}. * @@ -310,7 +359,7 @@ public final class DvrContract { public static final String COLUMN_START_FROM_SEASON = "start_from_season"; /** - * The number of the earliest episode to record in {@link #COLUMN_START_FROM_SEASON}. The + * The number of the earliest episode to record in {@link #COLUMN_START_FROM_SEASON}. The * value {@link #THE_BEGINNING} means record all episodes. * * <p>Default value is {@value #THE_BEGINNING} {@link #THE_BEGINNING}. @@ -322,8 +371,8 @@ public final class DvrContract { /** * The series recording option which indicates the channels to record. * - * <p>This value should be one of the followings: {@link #OPTION_CHANNEL_ONE} and - * {@link #OPTION_CHANNEL_ALL}. The default value is OPTION_CHANNEL_ONE. + * <p>This value should be one of the followings: {@link #OPTION_CHANNEL_ONE} and {@link + * #OPTION_CHANNEL_ALL}. The default value is OPTION_CHANNEL_ONE. * * <p>Type: TEXT */ @@ -338,6 +387,7 @@ public final class DvrContract { * to get the canonical genre strings from the text stored in the column. * * <p>Type: TEXT + * * @see android.media.tv.TvContract.Programs.Genres * @see android.media.tv.TvContract.Programs.Genres#encode * @see android.media.tv.TvContract.Programs.Genres#decode @@ -350,10 +400,9 @@ public final class DvrContract { * <p>The data in the column must be a URL, or a URI in one of the following formats: * * <ul> - * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> - * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) - * </li> - * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li> + * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT}) + * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) + * <li>file ({@link android.content.ContentResolver#SCHEME_FILE}) * </ul> * * <p>Type: TEXT @@ -366,10 +415,9 @@ public final class DvrContract { * <p>The data in the column must be a URL, or a URI in one of the following formats: * * <ul> - * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> - * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) - * </li> - * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li> + * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT}) + * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) + * <li>file ({@link android.content.ContentResolver#SCHEME_FILE}) * </ul> * * <p>Type: TEXT @@ -379,15 +427,15 @@ public final class DvrContract { /** * The state of whether the series recording be canceled or not. * - * <p>This value should be one of the followings: {@link #STATE_SERIES_NORMAL} and - * {@link #STATE_SERIES_STOPPED}. The default value is STATE_SERIES_NORMAL. + * <p>This value should be one of the followings: {@link #STATE_SERIES_NORMAL} and {@link + * #STATE_SERIES_STOPPED}. The default value is STATE_SERIES_NORMAL. * * <p>Type: TEXT */ public static final String COLUMN_STATE = "state"; - private SeriesRecordings() { } + private SeriesRecordings() {} } - private DvrContract() { } + private DvrContract() {} } diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 8b9481a9..41e5a66a 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -26,98 +26,145 @@ import android.database.sqlite.SQLiteStatement; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; - import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; -/** - * A data class for one recorded contents. - */ +/** A data class for one recorded contents. */ public class DvrDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "DvrDatabaseHelper"; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 18; private static final String DB_NAME = "dvr.db"; private static final String SQL_CREATE_SCHEDULES = - "CREATE TABLE " + Schedules.TABLE_NAME + "(" - + Schedules._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + Schedules.COLUMN_PRIORITY + " INTEGER DEFAULT " - + ScheduledRecording.DEFAULT_PRIORITY + "," - + Schedules.COLUMN_TYPE + " TEXT NOT NULL," - + Schedules.COLUMN_INPUT_ID + " TEXT NOT NULL," - + Schedules.COLUMN_CHANNEL_ID + " INTEGER NOT NULL," - + Schedules.COLUMN_PROGRAM_ID + " INTEGER," - + Schedules.COLUMN_PROGRAM_TITLE + " TEXT," - + Schedules.COLUMN_START_TIME_UTC_MILLIS + " INTEGER NOT NULL," - + Schedules.COLUMN_END_TIME_UTC_MILLIS + " INTEGER NOT NULL," - + Schedules.COLUMN_SEASON_NUMBER + " TEXT," - + Schedules.COLUMN_EPISODE_NUMBER + " TEXT," - + Schedules.COLUMN_EPISODE_TITLE + " TEXT," - + Schedules.COLUMN_PROGRAM_DESCRIPTION + " TEXT," - + Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION + " TEXT," - + Schedules.COLUMN_PROGRAM_POST_ART_URI + " TEXT," - + Schedules.COLUMN_PROGRAM_THUMBNAIL_URI + " TEXT," - + Schedules.COLUMN_STATE + " TEXT NOT NULL," - + Schedules.COLUMN_SERIES_RECORDING_ID + " INTEGER," - + "FOREIGN KEY(" + Schedules.COLUMN_SERIES_RECORDING_ID + ") " - + "REFERENCES " + SeriesRecordings.TABLE_NAME - + "(" + SeriesRecordings._ID + ") " + "CREATE TABLE " + + Schedules.TABLE_NAME + + "(" + + Schedules._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Schedules.COLUMN_PRIORITY + + " INTEGER DEFAULT " + + ScheduledRecording.DEFAULT_PRIORITY + + "," + + Schedules.COLUMN_TYPE + + " TEXT NOT NULL," + + Schedules.COLUMN_INPUT_ID + + " TEXT NOT NULL," + + Schedules.COLUMN_CHANNEL_ID + + " INTEGER NOT NULL," + + Schedules.COLUMN_PROGRAM_ID + + " INTEGER," + + Schedules.COLUMN_PROGRAM_TITLE + + " TEXT," + + Schedules.COLUMN_START_TIME_UTC_MILLIS + + " INTEGER NOT NULL," + + Schedules.COLUMN_END_TIME_UTC_MILLIS + + " INTEGER NOT NULL," + + Schedules.COLUMN_SEASON_NUMBER + + " TEXT," + + Schedules.COLUMN_EPISODE_NUMBER + + " TEXT," + + Schedules.COLUMN_EPISODE_TITLE + + " TEXT," + + Schedules.COLUMN_PROGRAM_DESCRIPTION + + " TEXT," + + Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION + + " TEXT," + + Schedules.COLUMN_PROGRAM_POST_ART_URI + + " TEXT," + + Schedules.COLUMN_PROGRAM_THUMBNAIL_URI + + " TEXT," + + Schedules.COLUMN_STATE + + " TEXT NOT NULL," + + Schedules.COLUMN_SERIES_RECORDING_ID + + " INTEGER," + + "FOREIGN KEY(" + + Schedules.COLUMN_SERIES_RECORDING_ID + + ") " + + "REFERENCES " + + SeriesRecordings.TABLE_NAME + + "(" + + SeriesRecordings._ID + + ") " + "ON UPDATE CASCADE ON DELETE SET NULL);"; private static final String SQL_DROP_SCHEDULES = "DROP TABLE IF EXISTS " + Schedules.TABLE_NAME; private static final String SQL_CREATE_SERIES_RECORDINGS = - "CREATE TABLE " + SeriesRecordings.TABLE_NAME + "(" - + SeriesRecordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + SeriesRecordings.COLUMN_PRIORITY + " INTEGER DEFAULT " - + SeriesRecording.DEFAULT_PRIORITY + "," - + SeriesRecordings.COLUMN_TITLE + " TEXT NOT NULL," - + SeriesRecordings.COLUMN_SHORT_DESCRIPTION + " TEXT," - + SeriesRecordings.COLUMN_LONG_DESCRIPTION + " TEXT," - + SeriesRecordings.COLUMN_INPUT_ID + " TEXT NOT NULL," - + SeriesRecordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL," - + SeriesRecordings.COLUMN_SERIES_ID + " TEXT NOT NULL," - + SeriesRecordings.COLUMN_START_FROM_SEASON + " INTEGER DEFAULT " - + SeriesRecordings.THE_BEGINNING + "," - + SeriesRecordings.COLUMN_START_FROM_EPISODE + " INTEGER DEFAULT " - + SeriesRecordings.THE_BEGINNING + "," - + SeriesRecordings.COLUMN_CHANNEL_OPTION + " TEXT DEFAULT " - + SeriesRecordings.OPTION_CHANNEL_ONE + "," - + SeriesRecordings.COLUMN_CANONICAL_GENRE + " TEXT," - + SeriesRecordings.COLUMN_POSTER_URI + " TEXT," - + SeriesRecordings.COLUMN_PHOTO_URI + " TEXT," - + SeriesRecordings.COLUMN_STATE + " TEXT)"; + "CREATE TABLE " + + SeriesRecordings.TABLE_NAME + + "(" + + SeriesRecordings._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + SeriesRecordings.COLUMN_PRIORITY + + " INTEGER DEFAULT " + + SeriesRecording.DEFAULT_PRIORITY + + "," + + SeriesRecordings.COLUMN_TITLE + + " TEXT NOT NULL," + + SeriesRecordings.COLUMN_SHORT_DESCRIPTION + + " TEXT," + + SeriesRecordings.COLUMN_LONG_DESCRIPTION + + " TEXT," + + SeriesRecordings.COLUMN_INPUT_ID + + " TEXT NOT NULL," + + SeriesRecordings.COLUMN_CHANNEL_ID + + " INTEGER NOT NULL," + + SeriesRecordings.COLUMN_SERIES_ID + + " TEXT NOT NULL," + + SeriesRecordings.COLUMN_START_FROM_SEASON + + " INTEGER DEFAULT " + + SeriesRecordings.THE_BEGINNING + + "," + + SeriesRecordings.COLUMN_START_FROM_EPISODE + + " INTEGER DEFAULT " + + SeriesRecordings.THE_BEGINNING + + "," + + SeriesRecordings.COLUMN_CHANNEL_OPTION + + " TEXT DEFAULT " + + SeriesRecordings.OPTION_CHANNEL_ONE + + "," + + SeriesRecordings.COLUMN_CANONICAL_GENRE + + " TEXT," + + SeriesRecordings.COLUMN_POSTER_URI + + " TEXT," + + SeriesRecordings.COLUMN_PHOTO_URI + + " TEXT," + + SeriesRecordings.COLUMN_STATE + + " TEXT)"; - private static final String SQL_DROP_SERIES_RECORDINGS = "DROP TABLE IF EXISTS " + - SeriesRecordings.TABLE_NAME; + private static final String SQL_DROP_SERIES_RECORDINGS = + "DROP TABLE IF EXISTS " + SeriesRecordings.TABLE_NAME; private static final int SQL_DATA_TYPE_LONG = 0; private static final int SQL_DATA_TYPE_INT = 1; private static final int SQL_DATA_TYPE_STRING = 2; - private static final ColumnInfo[] COLUMNS_SCHEDULES = new ColumnInfo[] { - new ColumnInfo(Schedules._ID, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_TYPE, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_PROGRAM_ID, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_PROGRAM_TITLE, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_START_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_END_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG), - new ColumnInfo(Schedules.COLUMN_SEASON_NUMBER, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_EPISODE_NUMBER, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_EPISODE_TITLE, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_PROGRAM_DESCRIPTION, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_PROGRAM_POST_ART_URI, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_STATE, SQL_DATA_TYPE_STRING), - new ColumnInfo(Schedules.COLUMN_SERIES_RECORDING_ID, SQL_DATA_TYPE_LONG)}; + private static final ColumnInfo[] COLUMNS_SCHEDULES = + new ColumnInfo[] { + new ColumnInfo(Schedules._ID, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_TYPE, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_PROGRAM_ID, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_PROGRAM_TITLE, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_START_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_END_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG), + new ColumnInfo(Schedules.COLUMN_SEASON_NUMBER, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_EPISODE_NUMBER, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_EPISODE_TITLE, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_PROGRAM_DESCRIPTION, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_PROGRAM_POST_ART_URI, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_STATE, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_FAILED_REASON, SQL_DATA_TYPE_STRING), + new ColumnInfo(Schedules.COLUMN_SERIES_RECORDING_ID, SQL_DATA_TYPE_LONG) + }; private static final String SQL_INSERT_SCHEDULES = buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES); @@ -125,22 +172,24 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES); private static final String SQL_DELETE_SCHEDULES = buildDeleteSql(Schedules.TABLE_NAME); - private static final ColumnInfo[] COLUMNS_SERIES_RECORDINGS = new ColumnInfo[] { - new ColumnInfo(SeriesRecordings._ID, SQL_DATA_TYPE_LONG), - new ColumnInfo(SeriesRecordings.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG), - new ColumnInfo(SeriesRecordings.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG), - new ColumnInfo(SeriesRecordings.COLUMN_SERIES_ID, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_TITLE, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_SEASON, SQL_DATA_TYPE_INT), - new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_EPISODE, SQL_DATA_TYPE_INT), - new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_OPTION, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_CANONICAL_GENRE, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_POSTER_URI, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_PHOTO_URI, SQL_DATA_TYPE_STRING), - new ColumnInfo(SeriesRecordings.COLUMN_STATE, SQL_DATA_TYPE_STRING)}; + private static final ColumnInfo[] COLUMNS_SERIES_RECORDINGS = + new ColumnInfo[] { + new ColumnInfo(SeriesRecordings._ID, SQL_DATA_TYPE_LONG), + new ColumnInfo(SeriesRecordings.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG), + new ColumnInfo(SeriesRecordings.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG), + new ColumnInfo(SeriesRecordings.COLUMN_SERIES_ID, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_TITLE, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_SEASON, SQL_DATA_TYPE_INT), + new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_EPISODE, SQL_DATA_TYPE_INT), + new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_OPTION, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_CANONICAL_GENRE, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_POSTER_URI, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_PHOTO_URI, SQL_DATA_TYPE_STRING), + new ColumnInfo(SeriesRecordings.COLUMN_STATE, SQL_DATA_TYPE_STRING) + }; private static final String SQL_INSERT_SERIES_RECORDINGS = buildInsertSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS); @@ -186,6 +235,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { private static String buildDeleteSql(String tableName) { return "DELETE FROM " + tableName + " WHERE " + BaseColumns._ID + "=?"; } + public DvrDatabaseHelper(Context context) { super(context.getApplicationContext(), DB_NAME, null, DATABASE_VERSION); } @@ -205,16 +255,20 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES); - db.execSQL(SQL_DROP_SCHEDULES); - if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS); - db.execSQL(SQL_DROP_SERIES_RECORDINGS); - onCreate(db); + if (oldVersion < 17) { + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES); + db.execSQL(SQL_DROP_SCHEDULES); + if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS); + db.execSQL(SQL_DROP_SERIES_RECORDINGS); + onCreate(db); + } + if (oldVersion < 18) { + db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN " + + Schedules.COLUMN_FAILED_REASON + " TEXT DEFAULT null;"); + } } - /** - * Handles the query request and returns a {@link Cursor}. - */ + /** Handles the query request and returns a {@link Cursor}. */ public Cursor query(String tableName, String[] projections) { SQLiteDatabase db = getReadableDatabase(); SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); @@ -222,9 +276,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { return builder.query(db, projections, null, null, null, null, null); } - /** - * Inserts schedules. - */ + /** Inserts schedules. */ public void insertSchedules(ScheduledRecording... scheduledRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_INSERT_SCHEDULES); @@ -242,9 +294,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - /** - * Update schedules. - */ + /** Update schedules. */ public void updateSchedules(ScheduledRecording... scheduledRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SCHEDULES); @@ -263,9 +313,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - /** - * Delete schedules. - */ + /** Delete schedules. */ public void deleteSchedules(ScheduledRecording... scheduledRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_DELETE_SCHEDULES); @@ -282,9 +330,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - /** - * Inserts series recordings. - */ + /** Inserts series recordings. */ public void insertSeriesRecordings(SeriesRecording... seriesRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_INSERT_SERIES_RECORDINGS); @@ -302,9 +348,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - /** - * Update series recordings. - */ + /** Update series recordings. */ public void updateSeriesRecordings(SeriesRecording... seriesRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SERIES_RECORDINGS); @@ -323,9 +367,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - /** - * Delete series recordings. - */ + /** Delete series recordings. */ public void deleteSeriesRecordings(SeriesRecording... seriesRecordings) { SQLiteDatabase db = getWritableDatabase(); SQLiteStatement statement = db.compileStatement(SQL_DELETE_SERIES_RECORDINGS); @@ -342,8 +384,8 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { } } - private void bindColumns(SQLiteStatement statement, ColumnInfo[] columns, - ContentValues values) { + private void bindColumns( + SQLiteStatement statement, ColumnInfo[] columns, ContentValues values) { for (int i = 0; i < columns.length; ++i) { ColumnInfo columnInfo = columns[i]; Object value = values.get(columnInfo.name); @@ -362,14 +404,15 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper { statement.bindLong(i + 1, (Integer) value); } break; - case SQL_DATA_TYPE_STRING: { - if (TextUtils.isEmpty((String) value)) { - statement.bindNull(i + 1); - } else { - statement.bindString(i + 1, (String) value); + case SQL_DATA_TYPE_STRING: + { + if (TextUtils.isEmpty((String) value)) { + statement.bindNull(i + 1); + } else { + statement.bindString(i + 1, (String) value); + } + break; } - break; - } } } } diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java index ff391959..42bc8bcc 100644 --- a/src/com/android/tv/dvr/provider/DvrDbSync.java +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -29,8 +29,7 @@ import android.os.Looper; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; import android.util.Log; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; @@ -41,7 +40,6 @@ import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; import com.android.tv.util.TvUriMatcher; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -50,6 +48,7 @@ import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.Set; +import java.util.concurrent.Executor; /** * A class to synchronizes DVR DB with TvProvider. @@ -70,28 +69,31 @@ public class DvrDbSync { private final DvrManager mDvrManager; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; + private final Executor mDbExecutor; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); private QueryProgramTask mQueryProgramTask; private final SeriesRecordingScheduler mSeriesRecordingScheduler; - private final ContentObserver mContentObserver = new ContentObserver(new Handler( - Looper.getMainLooper())) { - @SuppressLint("SwitchIntDef") - @Override - public void onChange(boolean selfChange, Uri uri) { - switch (TvUriMatcher.match(uri)) { - case TvUriMatcher.MATCH_PROGRAM: - if (DEBUG) Log.d(TAG, "onProgramsUpdated"); - onProgramsUpdated(); - break; - case TvUriMatcher.MATCH_PROGRAM_ID: - if (DEBUG) { - Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); + private final ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @SuppressLint("SwitchIntDef") + @Override + public void onChange(boolean selfChange, Uri uri) { + switch (TvUriMatcher.match(uri)) { + case TvUriMatcher.MATCH_PROGRAM: + if (DEBUG) Log.d(TAG, "onProgramsUpdated"); + onProgramsUpdated(); + break; + case TvUriMatcher.MATCH_PROGRAM_ID: + if (DEBUG) { + Log.d( + TAG, + "onProgramUpdated: programId=" + ContentUris.parseId(uri)); + } + onProgramUpdated(ContentUris.parseId(uri)); + break; } - onProgramUpdated(ContentUris.parseId(uri)); - break; - } - } - }; + } + }; private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @@ -106,70 +108,76 @@ public class DvrDbSync { } @Override - public void onChannelBrowsableChanged() { } + public void onChannelBrowsableChanged() {} }; - private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - addProgramIdToCheckIfNeeded(schedule); - } - startNextUpdateIfNeeded(); - } + private final ScheduledRecordingListener mScheduleListener = + new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + addProgramIdToCheckIfNeeded(schedule); + } + startNextUpdateIfNeeded(); + } - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - mProgramIdQueue.remove(schedule.getProgramId()); - } - } + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + } + } - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - mProgramIdQueue.remove(schedule.getProgramId()); - addProgramIdToCheckIfNeeded(schedule); - } - startNextUpdateIfNeeded(); - } - }; + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + addProgramIdToCheckIfNeeded(schedule); + } + startNextUpdateIfNeeded(); + } + }; public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { - this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(), - TvApplication.getSingletons(context).getDvrManager(), - SeriesRecordingScheduler.getInstance(context)); + this( + context, + dataManager, + TvSingletons.getSingletons(context).getChannelDataManager(), + TvSingletons.getSingletons(context).getDvrManager(), + SeriesRecordingScheduler.getInstance(context), + TvSingletons.getSingletons(context).getDbExecutor()); } @VisibleForTesting - DvrDbSync(Context context, DvrDataManagerImpl dataManager, - ChannelDataManager channelDataManager, DvrManager dvrManager, - SeriesRecordingScheduler seriesRecordingScheduler) { + DvrDbSync( + Context context, + DvrDataManagerImpl dataManager, + ChannelDataManager channelDataManager, + DvrManager dvrManager, + SeriesRecordingScheduler seriesRecordingScheduler, + Executor dbExecutor) { mContext = context; mDvrManager = dvrManager; mDataManager = dataManager; mChannelDataManager = channelDataManager; mSeriesRecordingScheduler = seriesRecordingScheduler; + mDbExecutor = dbExecutor; } - /** - * Starts the DB sync. - */ + /** Starts the DB sync. */ public void start() { if (!mChannelDataManager.isDbLoadFinished()) { mChannelDataManager.addListener(mChannelDataManagerListener); return; } - mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, - mContentObserver); + mContext.getContentResolver() + .registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); mDataManager.addScheduledRecordingListener(mScheduleListener); onChannelsUpdated(); onProgramsUpdated(); } - /** - * Stops the DB sync. - */ + /** Stops the DB sync. */ public void stop() { mProgramIdQueue.clear(); if (mQueryProgramTask != null) { @@ -185,14 +193,15 @@ public class DvrDbSync { for (SeriesRecording r : mDataManager.getSeriesRecordings()) { if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { - seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r) - .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) - .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + seriesRecordingsToUpdate.add( + SeriesRecording.buildFrom(r) + .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) + .setState(SeriesRecording.STATE_SERIES_STOPPED) + .build()); } } if (!seriesRecordingsToUpdate.isEmpty()) { - mDataManager.updateSeriesRecording( - SeriesRecording.toArray(seriesRecordingsToUpdate)); + mDataManager.updateSeriesRecording(SeriesRecording.toArray(seriesRecordingsToUpdate)); } List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { @@ -202,8 +211,7 @@ public class DvrDbSync { } } if (!schedulesToRemove.isEmpty()) { - mDataManager.removeScheduledRecording( - ScheduledRecording.toArray(schedulesToRemove)); + mDataManager.removeScheduledRecording(ScheduledRecording.toArray(schedulesToRemove)); } } @@ -227,7 +235,7 @@ public class DvrDbSync { if (programId != ScheduledRecording.ID_NOT_SET && !mProgramIdQueue.contains(programId) && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); mProgramIdQueue.offer(programId); // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the @@ -258,7 +266,7 @@ public class DvrDbSync { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); if (schedule != null && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { if (program == null) { mDataManager.removeScheduledRecording(schedule); if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { @@ -270,15 +278,16 @@ public class DvrDbSync { } } else { long currentTimeMs = System.currentTimeMillis(); - ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) - .setEndTimeMs(program.getEndTimeUtcMillis()) - .setSeasonNumber(program.getSeasonNumber()) - .setEpisodeNumber(program.getEpisodeNumber()) - .setEpisodeTitle(program.getEpisodeTitle()) - .setProgramDescription(program.getDescription()) - .setProgramLongDescription(program.getLongDescription()) - .setProgramPosterArtUri(program.getPosterArtUri()) - .setProgramThumbnailUri(program.getThumbnailUri()); + ScheduledRecording.Builder builder = + ScheduledRecording.buildFrom(schedule) + .setEndTimeMs(program.getEndTimeUtcMillis()) + .setSeasonNumber(program.getSeasonNumber()) + .setEpisodeNumber(program.getEpisodeNumber()) + .setEpisodeTitle(program.getEpisodeTitle()) + .setProgramDescription(program.getDescription()) + .setProgramLongDescription(program.getLongDescription()) + .setProgramPosterArtUri(program.getPosterArtUri()) + .setProgramThumbnailUri(program.getThumbnailUri()); boolean needUpdate = false; // Check the series recording. SeriesRecording seriesRecordingForOldSchedule = @@ -289,9 +298,11 @@ public class DvrDbSync { mDataManager.getSeriesRecording(program.getSeriesId()); if (seriesRecording == null) { // The new program is episodic while the previous one isn't. - SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording( - program, Collections.singletonList(program), - SeriesRecording.STATE_SERIES_STOPPED); + SeriesRecording newSeriesRecording = + mDvrManager.addSeriesRecording( + program, + Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); builder.setSeriesRecordingId(newSeriesRecording.getId()); needUpdate = true; } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { @@ -302,10 +313,10 @@ public class DvrDbSync { if (seriesRecordingForOldSchedule != null) { seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); } - } else if (!Objects.equals(schedule.getSeasonNumber(), - program.getSeasonNumber()) - || !Objects.equals(schedule.getEpisodeNumber(), - program.getEpisodeNumber())) { + } else if (!Objects.equals( + schedule.getSeasonNumber(), program.getSeasonNumber()) + || !Objects.equals( + schedule.getEpisodeNumber(), program.getEpisodeNumber())) { // The episode number has been changed. if (seriesRecordingForOldSchedule != null) { seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); @@ -318,23 +329,24 @@ public class DvrDbSync { // Change start time only when the recording is not started yet. boolean needToChangeStartTime = schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS - && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); + && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { builder.setStartTimeMs(program.getStartTimeUtcMillis()); needUpdate = true; } - if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() + if (needUpdate + || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) - || !Objects.equals(schedule.getProgramDescription(), - program.getDescription()) - || !Objects.equals(schedule.getProgramLongDescription(), - program.getLongDescription()) - || !Objects.equals(schedule.getProgramPosterArtUri(), - program.getPosterArtUri()) - || !Objects.equals(schedule.getProgramThumbnailUri(), - program.getThumbnailUri())) { + || !Objects.equals( + schedule.getProgramDescription(), program.getDescription()) + || !Objects.equals( + schedule.getProgramLongDescription(), program.getLongDescription()) + || !Objects.equals( + schedule.getProgramPosterArtUri(), program.getPosterArtUri()) + || !Objects.equals( + schedule.getProgramThumbnailUri(), program.getThumbnailUri())) { mDataManager.updateScheduledRecording(builder.build()); } if (!seriesRecordingsToUpdate.isEmpty()) { @@ -349,7 +361,7 @@ public class DvrDbSync { private final long mProgramId; QueryProgramTask(long programId) { - super(mContext.getContentResolver(), programId); + super(mDbExecutor, mContext.getContentResolver(), programId); mProgramId = programId; } diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java index ba0aca51..b7d9f3b3 100644 --- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java +++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java @@ -25,18 +25,16 @@ import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.data.SeasonEpisodeNumber; import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeasonEpisodeNumber; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; import com.android.tv.util.AsyncDbTask.CursorFilter; -import com.android.tv.util.PermissionUtils; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -44,11 +42,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -/** - * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. - */ +/** A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. */ @TargetApi(Build.VERSION_CODES.N) -abstract public class EpisodicProgramLoadTask { +public abstract class EpisodicProgramLoadTask { private static final String TAG = "EpisodicProgramLoadTask"; private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); @@ -61,11 +57,15 @@ abstract public class EpisodicProgramLoadTask { private static final String PARAM_END_TIME = "end_time"; private static final String PROGRAM_PREDICATE = - Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND " - + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + Programs.COLUMN_START_TIME_UTC_MILLIS + + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + + "=0"; private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = - Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND " - + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + Programs.COLUMN_END_TIME_UTC_MILLIS + + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + + "=0"; private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; @@ -80,10 +80,7 @@ abstract public class EpisodicProgramLoadTask { private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>(); private AsyncProgramQueryTask mProgramQueryTask; - /** - * - * Constructor used to load programs for one series recording with the given channel option. - */ + /** Constructor used to load programs for one series recording with the given channel option. */ public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { this(context, Collections.singletonList(seriesRecording)); } @@ -94,64 +91,56 @@ abstract public class EpisodicProgramLoadTask { */ public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) { mContext = context.getApplicationContext(); - mDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); mSeriesRecordings.addAll(seriesRecordings); } - /** - * Returns the series recordings. - */ + /** Returns the series recordings. */ public List<SeriesRecording> getSeriesRecordings() { return mSeriesRecordings; } - /** - * Returns the program query task. It is {@code null} until it is executed. - */ + /** Returns the program query task. It is {@code null} until it is executed. */ @Nullable public AsyncProgramQueryTask getTask() { return mProgramQueryTask; } - /** - * Enables loading current programs. The default value is {@code false}. - */ + /** Enables loading current programs. The default value is {@code false}. */ public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); + SoftPreconditions.checkState( + mProgramQueryTask == null, TAG, "Can't change setting after execution."); mLoadCurrentProgram = loadCurrentProgram; return this; } - /** - * Enables already schedules episodes. The default value is {@code false}. - */ + /** Enables already schedules episodes. The default value is {@code false}. */ public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); + SoftPreconditions.checkState( + mProgramQueryTask == null, TAG, "Can't change setting after execution."); mLoadScheduledEpisode = loadScheduledEpisode; return this; } /** - * Enables loading disallowed programs whose schedules were removed manually by the user. - * The default value is {@code false}. + * Enables loading disallowed programs whose schedules were removed manually by the user. The + * default value is {@code false}. */ public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); + SoftPreconditions.checkState( + mProgramQueryTask == null, TAG, "Can't change setting after execution."); mLoadDisallowedProgram = loadDisallowedProgram; return this; } /** - * Gives the option whether to ignore the channel option when matching programs. - * If {@code ignoreChannelOption} is {@code true}, the program will be matched with - * {@link SeriesRecording#OPTION_CHANNEL_ALL} option. + * Gives the option whether to ignore the channel option when matching programs. If {@code + * ignoreChannelOption} is {@code true}, the program will be matched with {@link + * SeriesRecording#OPTION_CHANNEL_ALL} option. */ public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); + SoftPreconditions.checkState( + mProgramQueryTask == null, TAG, "Can't change setting after execution."); mIgnoreChannelOption = ignoreChannelOption; return this; } @@ -162,12 +151,15 @@ abstract public class EpisodicProgramLoadTask { * @see com.android.tv.util.AsyncDbTask#executeOnDbThread */ public void execute() { - if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + if (SoftPreconditions.checkState( + mProgramQueryTask == null, + TAG, "Can't execute task: the task is already running.")) { - mQueryAllChannels = mSeriesRecordings.size() > 1 - || mSeriesRecordings.get(0).getChannelOption() - == SeriesRecording.OPTION_CHANNEL_ALL - || mIgnoreChannelOption; + mQueryAllChannels = + mSeriesRecordings.size() > 1 + || mSeriesRecordings.get(0).getChannelOption() + == SeriesRecording.OPTION_CHANNEL_ALL + || mIgnoreChannelOption; mProgramQueryTask = createTask(); mProgramQueryTask.executeOnDbThread(); } @@ -184,22 +176,22 @@ abstract public class EpisodicProgramLoadTask { } } - /** - * Runs on the UI thread after the program loading finishes successfully. - */ - protected void onPostExecute(List<Program> programs) { - } + /** Runs on the UI thread after the program loading finishes successfully. */ + protected void onPostExecute(List<Program> programs) {} - /** - * Runs on the UI thread after the program loading was canceled. - */ - protected void onCancelled(List<Program> programs) { - } + /** Runs on the UI thread after the program loading was canceled. */ + protected void onCancelled(List<Program> programs) {} private AsyncProgramQueryTask createTask() { SqlParams sqlParams = createSqlParams(); - return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, - sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) { + return new AsyncProgramQueryTask( + TvSingletons.getSingletons(mContext).getDbExecutor(), + mContext.getContentResolver(), + sqlParams.uri, + sqlParams.selection, + sqlParams.selectionArgs, + null, + sqlParams.filter) { @Override protected void onPostExecute(List<Program> programs) { EpisodicProgramLoadTask.this.onPostExecute(programs); @@ -217,8 +209,11 @@ abstract public class EpisodicProgramLoadTask { if (PermissionUtils.hasAccessAllEpg(mContext)) { sqlParams.uri = Programs.CONTENT_URI; // Base - StringBuilder selection = new StringBuilder(mLoadCurrentProgram - ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE); + StringBuilder selection = + new StringBuilder( + mLoadCurrentProgram + ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM + : PROGRAM_PREDICATE); List<String> args = new ArrayList<>(); args.add(Long.toString(System.currentTimeMillis())); // Channel option @@ -237,15 +232,21 @@ abstract public class EpisodicProgramLoadTask { } else { // The query includes the current program. Will be filtered if needed. if (mQueryAllChannels) { - sqlParams.uri = Programs.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_START_TIME, - String.valueOf(System.currentTimeMillis())) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) - .build(); + sqlParams.uri = + Programs.CONTENT_URI + .buildUpon() + .appendQueryParameter( + PARAM_START_TIME, + String.valueOf(System.currentTimeMillis())) + .appendQueryParameter( + PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) + .build(); } else { - sqlParams.uri = TvContract.buildProgramsUriForChannel( - mSeriesRecordings.get(0).getChannelId(), - System.currentTimeMillis(), Long.MAX_VALUE); + sqlParams.uri = + TvContract.buildProgramsUriForChannel( + mSeriesRecordings.get(0).getChannelId(), + System.currentTimeMillis(), + Long.MAX_VALUE); } sqlParams.selection = null; sqlParams.selectionArgs = null; @@ -292,16 +293,19 @@ abstract public class EpisodicProgramLoadTask { for (SeriesRecording seriesRecording : mSeriesRecordings) { boolean programMatches; if (mIgnoreChannelOption) { - programMatches = seriesRecording.matchProgram(program, - SeriesRecording.OPTION_CHANNEL_ALL); + programMatches = + seriesRecording.matchProgram( + program, SeriesRecording.OPTION_CHANNEL_ALL); } else { programMatches = seriesRecording.matchProgram(program); } if (programMatches) { return mLoadScheduledEpisode - || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber())); + || !mSeasonEpisodeNumbers.contains( + new SeasonEpisodeNumber( + seriesRecording.getId(), + program.getSeasonNumber(), + program.getEpisodeNumber())); } } return false; @@ -316,7 +320,8 @@ abstract public class EpisodicProgramLoadTask { @Override public boolean filter(Cursor c) { return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) - && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); + && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 + && super.filter(c); } } diff --git a/src/com/android/tv/dvr/recorder/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java index 8aa90116..bfd315e9 100644 --- a/src/com/android/tv/dvr/recorder/ConflictChecker.java +++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java @@ -27,21 +27,19 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.ArraySet; import android.util.Log; - -import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; import com.android.tv.MainActivity; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrUiHelper; - import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,8 +48,9 @@ import java.util.concurrent.TimeUnit; /** * Checking the runtime conflict of DVR recording. - * <p> - * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. + * + * <p>This class runs only while the {@link MainActivity} is resumed and holds the upcoming + * conflicts. */ @TargetApi(Build.VERSION_CODES.N) @MainThread @@ -87,24 +86,40 @@ public class ConflictChecker { private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + if (DEBUG) { + Log.d( + TAG, + "onScheduledRecordingAdded: " + + Arrays.toString(scheduledRecordings)); + } + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + if (DEBUG) { + Log.d( + TAG, + "onScheduledRecordingRemoved: " + + Arrays.toString(scheduledRecordings)); + } + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - }; + @Override + public void onScheduledRecordingStatusChanged( + ScheduledRecording... scheduledRecordings) { + if (DEBUG) { + Log.d( + TAG, + "onScheduledRecordingStatusChanged: " + + Arrays.toString(scheduledRecordings)); + } + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } + }; private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = new OnTvViewChannelChangeListener() { @@ -118,15 +133,13 @@ public class ConflictChecker { public ConflictChecker(MainActivity mainActivity) { mMainActivity = mainActivity; - ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); - mChannelDataManager = appSingletons.getChannelDataManager(); - mScheduleManager = appSingletons.getDvrScheduleManager(); - mSessionManager = appSingletons.getInputSessionManager(); + TvSingletons tvSingletons = TvSingletons.getSingletons(mainActivity); + mChannelDataManager = tvSingletons.getChannelDataManager(); + mScheduleManager = tvSingletons.getDvrScheduleManager(); + mSessionManager = tvSingletons.getInputSessionManager(); } - /** - * Starts checking the conflict. - */ + /** Starts checking the conflict. */ public void start() { if (mStarted) { return; @@ -137,9 +150,7 @@ public class ConflictChecker { mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); } - /** - * Stops checking the conflict. - */ + /** Stops checking the conflict. */ public void stop() { if (!mStarted) { return; @@ -150,23 +161,17 @@ public class ConflictChecker { mHandler.removeCallbacksAndMessages(null); } - /** - * Returns the upcoming conflicts. - */ + /** Returns the upcoming conflicts. */ public List<ScheduledRecording> getUpcomingConflicts() { return new ArrayList<>(mUpcomingConflicts); } - /** - * Adds a {@link OnUpcomingConflictChangeListener}. - */ + /** Adds a {@link OnUpcomingConflictChangeListener}. */ public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { mOnUpcomingConflictChangeListeners.add(listener); } - /** - * Removes the {@link OnUpcomingConflictChangeListener}. - */ + /** Removes the {@link OnUpcomingConflictChangeListener}. */ public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { mOnUpcomingConflictChangeListeners.remove(listener); } @@ -177,9 +182,7 @@ public class ConflictChecker { } } - /** - * Remembers the user's decision to record while watching the channel. - */ + /** Remembers the user's decision to record while watching the channel. */ public void setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts) { mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); } @@ -190,8 +193,7 @@ public class ConflictChecker { if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); mHandler.removeMessages(MSG_CHECK_CONFLICT); mUpcomingConflicts.clear(); - if (!mScheduleManager.isInitialized() - || !mChannelDataManager.isDbLoadFinished()) { + if (!mScheduleManager.isInitialized() || !mChannelDataManager.isDbLoadFinished()) { mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); notifyUpcomingConflictChanged(); return; @@ -209,8 +211,8 @@ public class ConflictChecker { long channelId = ContentUris.parseId(channelUri); Channel channel = mChannelDataManager.getChannel(channelId); // The conflicts caused by watching the channel. - List<ScheduledRecording> conflicts = mScheduleManager - .getConflictingSchedulesForWatching(channel.getId()); + List<ScheduledRecording> conflicts = + mScheduleManager.getConflictingSchedulesForWatching(channel.getId()); long earliestToCheck = Long.MAX_VALUE; long currentTimeMs = System.currentTimeMillis(); for (ScheduledRecording schedule : conflicts) { @@ -239,18 +241,15 @@ public class ConflictChecker { } } if (earliestToCheck != Long.MAX_VALUE) { - mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, - earliestToCheck - currentTimeMs); + mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, earliestToCheck - currentTimeMs); } if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); notifyUpcomingConflictChanged(); if (!mUpcomingConflicts.isEmpty() && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { // Don't show the conflict dialog if the user already knows. - List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get( - channel.getId()); - if (checkedConflicts == null - || !checkedConflicts.containsAll(mUpcomingConflicts)) { + List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get(channel.getId()); + if (checkedConflicts == null || !checkedConflicts.containsAll(mUpcomingConflicts)) { DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); } } @@ -271,9 +270,7 @@ public class ConflictChecker { } } - /** - * A listener for the change of upcoming conflicts. - */ + /** A listener for the change of upcoming conflicts. */ public interface OnUpcomingConflictChangeListener { void onUpcomingConflictChange(); } diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java index 5d324ca5..9fdbf062 100644 --- a/src/com/android/tv/dvr/recorder/DvrRecordingService.java +++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java @@ -29,21 +29,20 @@ import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.VisibleForTesting; import android.util.Log; - -import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.common.util.Clock; import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.util.Clock; import com.android.tv.util.RecurringRunner; /** - * DVR recording service. This service should be a foreground service and send a notification - * to users to do long-running recording task. + * DVR recording service. This service should be a foreground service and send a notification to + * users to do long-running recording task. * * <p>This service is waken up when there's a scheduled recording coming soon and at boot completed * since schedules have to be loaded from databases in order to set new recording alarms, which @@ -67,9 +66,9 @@ public class DvrRecordingService extends Service { /** * Starts the service in foreground. * - * @param startForRecording {@code true} if there are upcoming recordings in - * {@link RecordingScheduler#SOON_DURATION_IN_MS} and the service is - * started in foreground for those recordings. + * @param startForRecording {@code true} if there are upcoming recordings in {@link + * RecordingScheduler#SOON_DURATION_IN_MS} and the service is started in foreground for + * those recordings. */ @MainThread static void startForegroundService(Context context, boolean startForRecording) { @@ -99,7 +98,8 @@ public class DvrRecordingService extends Service { @VisibleForTesting boolean mIsRecording; private boolean mForeground; - @VisibleForTesting final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = + @VisibleForTesting + final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = new OnRecordingSessionChangeListener() { @Override public void onRecordingSessionChange(final boolean create, final int count) { @@ -114,18 +114,22 @@ public class DvrRecordingService extends Service { @Override public void onCreate() { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(); SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); sInstance = this; - ApplicationSingletons singletons = TvApplication.getSingletons(this); + TvSingletons singletons = TvSingletons.getSingletons(this); WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); mSessionManager = singletons.getInputSessionManager(); mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener); - mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), - new ScheduledProgramReaper(dataManager, Clock.SYSTEM), null); + mReaperRunner = + new RecurringRunner( + this, + java.util.concurrent.TimeUnit.DAYS.toMillis(1), + new ScheduledProgramReaper(dataManager, Clock.SYSTEM), + null); mReaperRunner.start(); mContentTitle = getString(R.string.dvr_notification_content_title); mContentTextRecording = getString(R.string.dvr_notification_content_text_recording); @@ -179,13 +183,16 @@ public class DvrRecordingService extends Service { @VisibleForTesting protected void startForegroundInternal(boolean hasUpcomingRecording) { - // STOPSHIP: Replace the content title with real UX strings - Notification.Builder builder = new Notification.Builder(this) - .setContentTitle(mContentTitle) - .setContentText(hasUpcomingRecording ? mContentTextRecording : mContentTextLoading) - .setSmallIcon(R.drawable.ic_dvr); - Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? - builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() : builder.build(); + Notification.Builder builder = + new Notification.Builder(this) + .setContentTitle(mContentTitle) + .setContentText( + hasUpcomingRecording ? mContentTextRecording : mContentTextLoading) + .setSmallIcon(R.drawable.ic_dvr); + Notification notification = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() + : builder.build(); startForeground(ONGOING_NOTIFICATION_ID, notification); } @@ -196,10 +203,11 @@ public class DvrRecordingService extends Service { private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // STOPSHIP: Replace the channel name with real UX strings - mNotificationChannel = new NotificationChannel(DVR_NOTIFICATION_CHANNEL_ID, - getString(R.string.dvr_notification_channel_name), - NotificationManager.IMPORTANCE_LOW); + mNotificationChannel = + new NotificationChannel( + DVR_NOTIFICATION_CHANNEL_ID, + getString(R.string.dvr_notification_channel_name), + NotificationManager.IMPORTANCE_LOW); ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) .createNotificationChannel(mNotificationChannel); } diff --git a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java index f1c0020b..bb5ea99d 100644 --- a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java +++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java @@ -21,18 +21,16 @@ import android.content.Context; import android.content.Intent; import android.os.Build; import android.support.annotation.RequiresApi; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; -import com.android.tv.TvApplication; - -/** - * Signals the DVR to start recording shows <i>soon</i>. - */ +/** Signals the DVR to start recording shows <i>soon</i>. */ @RequiresApi(Build.VERSION_CODES.N) public class DvrStartRecordingReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - TvApplication.setCurrentRunningProcess(context, true); - RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler(); + Starter.start(context); + RecordingScheduler scheduler = TvSingletons.getSingletons(context).getRecordingScheduler(); if (scheduler != null) { scheduler.updateAndStartServiceIfNeeded(); } diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java index fee4568e..1021b2bc 100644 --- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java @@ -25,17 +25,15 @@ import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; - import com.android.tv.InputSessionManager; -import com.android.tv.data.Channel; +import com.android.tv.common.util.Clock; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.util.Clock; import com.android.tv.util.CompositeComparator; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -43,9 +41,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -/** - * The scheduler for a TV input. - */ +/** The scheduler for a TV input. */ public class InputTaskScheduler { private static final String TAG = "InputTaskScheduler"; private static final boolean DEBUG = false; @@ -66,9 +62,7 @@ public class InputTaskScheduler { RecordingTask.END_TIME_COMPARATOR, RecordingTask.ID_COMPARATOR); - /** - * Returns the comparator which the schedules are sorted with when executed. - */ + /** Returns the comparator which the schedules are sorted with when executed. */ public static Comparator<ScheduledRecording> getRecordingOrderComparator() { return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; } @@ -81,8 +75,8 @@ public class InputTaskScheduler { private final long mId; private final RecordingTask mTask; - HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, - RecordingTask recordingTask) { + HandlerWrapper( + Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) { super(looper, recordingTask); mId = scheduledRecording.getId(); mTask = recordingTask; @@ -94,7 +88,7 @@ public class InputTaskScheduler { // The RecordingTask gets a chance first. // It must return false to pass this message to here. if (msg.what == MESSAGE_REMOVE) { - if (DEBUG) Log.d(TAG, "done " + mId); + if (DEBUG) Log.d(TAG, "done " + mId); mPendingRecordings.remove(mId); } removeCallbacksAndMessages(null); @@ -120,17 +114,37 @@ public class InputTaskScheduler { private final Object mInputLock = new Object(); private final RecordingTaskFactory mRecordingTaskFactory; - public InputTaskScheduler(Context context, TvInputInfo input, Looper looper, - ChannelDataManager channelDataManager, DvrManager dvrManager, - DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) { - this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager, - clock, null); + public InputTaskScheduler( + Context context, + TvInputInfo input, + Looper looper, + ChannelDataManager channelDataManager, + DvrManager dvrManager, + DvrDataManager dataManager, + InputSessionManager sessionManager, + Clock clock) { + this( + context, + input, + looper, + channelDataManager, + dvrManager, + dataManager, + sessionManager, + clock, + null); } @VisibleForTesting - InputTaskScheduler(Context context, TvInputInfo input, Looper looper, - ChannelDataManager channelDataManager, DvrManager dvrManager, - DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, + InputTaskScheduler( + Context context, + TvInputInfo input, + Looper looper, + ChannelDataManager channelDataManager, + DvrManager dvrManager, + DvrDataManager dataManager, + InputSessionManager sessionManager, + Clock clock, RecordingTaskFactory recordingTaskFactory) { if (DEBUG) Log.d(TAG, "Creating scheduler for " + input); mContext = context; @@ -142,22 +156,32 @@ public class InputTaskScheduler { mSessionManager = sessionManager; mClock = clock; mMainThreadHandler = new Handler(Looper.getMainLooper()); - mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory - : new RecordingTaskFactory() { - @Override - public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock) { - return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager, - mDataManager, mClock); - } - }; + mRecordingTaskFactory = + recordingTaskFactory != null + ? recordingTaskFactory + : new RecordingTaskFactory() { + @Override + public RecordingTask createRecordingTask( + ScheduledRecording schedule, + Channel channel, + DvrManager dvrManager, + InputSessionManager sessionManager, + WritableDvrDataManager dataManager, + Clock clock) { + return new RecordingTask( + mContext, + schedule, + channel, + mDvrManager, + mSessionManager, + mDataManager, + mClock); + } + }; mHandler = new WorkerThreadHandler(looper); } - /** - * Adds a {@link ScheduledRecording}. - */ + /** Adds a {@link ScheduledRecording}. */ public void addSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); } @@ -173,9 +197,7 @@ public class InputTaskScheduler { mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); } - /** - * Removes the {@link ScheduledRecording}. - */ + /** Removes the {@link ScheduledRecording}. */ public void removeSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); } @@ -194,9 +216,7 @@ public class InputTaskScheduler { } } - /** - * Updates the {@link ScheduledRecording}. - */ + /** Updates the {@link ScheduledRecording}. */ public void updateSchedule(ScheduledRecording schedule) { mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); } @@ -224,18 +244,14 @@ public class InputTaskScheduler { } } - /** - * Updates the TV input. - */ + /** Updates the TV input. */ public void updateTvInputInfo(TvInputInfo input) { synchronized (mInputLock) { mInput = input; } } - /** - * Stops the input task scheduler. - */ + /** Stops the input task scheduler. */ public void stop() { mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); @@ -262,7 +278,9 @@ public class InputTaskScheduler { ScheduledRecording schedule = iter.next(); if (schedule.getEndTimeMs() - currentTimeMs <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { - fail(schedule); + Log.e(TAG, "Error! Program ended before recording started:" + schedule); + fail(schedule, + ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED); iter.remove(); } } @@ -274,7 +292,8 @@ public class InputTaskScheduler { for (ScheduledRecording schedule : mWaitingSchedules.values()) { if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) { + <= currentTimeMs + && schedule.getEndTimeMs() > currentTimeMs) { schedulesToStart.add(schedule); } } @@ -321,10 +340,12 @@ public class InputTaskScheduler { earliest = schedule.getEndTimeMs(); } } else { - if (earliest > schedule.getStartTimeMs() - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { - earliest = schedule.getStartTimeMs() - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; + if (earliest + > schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { + earliest = + schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; } } } @@ -333,8 +354,9 @@ public class InputTaskScheduler { private RecordingTask createRecordingTask(ScheduledRecording schedule) { Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); - RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel, - mDvrManager, mSessionManager, mDataManager, mClock); + RecordingTask recordingTask = + mRecordingTaskFactory.createRecordingTask( + schedule, channel, mDvrManager, mSessionManager, mDataManager, mClock); HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask); mPendingRecordings.put(schedule.getId(), handlerWrapper); return recordingTask; @@ -369,21 +391,24 @@ public class InputTaskScheduler { return candidate; } - private void fail(ScheduledRecording schedule) { + private void fail(ScheduledRecording schedule, int reason) { // It's called when the scheduling has been failed without creating RecordingTask. - runOnMainHandler(new Runnable() { - @Override - public void run() { - ScheduledRecording scheduleInManager = - mDataManager.getScheduledRecording(schedule.getId()); - if (scheduleInManager != null) { - // The schedule should be updated based on the object from DataManager in case - // when it has been updated. - mDataManager.changeState(scheduleInManager, - ScheduledRecording.STATE_RECORDING_FAILED); - } - } - }); + runOnMainHandler( + new Runnable() { + @Override + public void run() { + ScheduledRecording scheduleInManager = + mDataManager.getScheduledRecording(schedule.getId()); + if (scheduleInManager != null) { + // The schedule should be updated based on the object from DataManager + // in case when it has been updated. + mDataManager.changeState( + scheduleInManager, + ScheduledRecording.STATE_RECORDING_FAILED, + reason); + } + } + }); } private void runOnMainHandler(Runnable runnable) { @@ -396,9 +421,13 @@ public class InputTaskScheduler { @VisibleForTesting interface RecordingTaskFactory { - RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock); + RecordingTask createRecordingTask( + ScheduledRecording scheduledRecording, + Channel channel, + DvrManager dvrManager, + InputSessionManager sessionManager, + WritableDvrDataManager dataManager, + Clock clock); } private class WorkerThreadHandler extends Handler { @@ -417,6 +446,7 @@ public class InputTaskScheduler { break; case MSG_UPDATE_SCHEDULED_RECORDING: handleUpdateSchedule((ScheduledRecording) msg.obj); + break; case MSG_BUILD_SCHEDULE: handleBuildSchedule(); break; diff --git a/src/com/android/tv/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java index cbaf46b5..f309537d 100644 --- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java @@ -31,11 +31,10 @@ import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; import android.util.Range; - -import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.Clock; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; import com.android.tv.dvr.DvrDataManager; @@ -44,22 +43,21 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.util.Clock; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** - * The core class to manage DVR schedule and run recording task. - ** - * <p> This class is responsible for: + * The core class to manage DVR schedule and run recording task. * + * + * <p>This class is responsible for: + * * <ul> - * <li>Sending record commands to TV inputs</li> - * <li>Resolving conflicting schedules, handling overlapping recording time durations, etc.</li> + * <li>Sending record commands to TV inputs + * <li>Resolving conflicting schedules, handling overlapping recording time durations, etc. * </ul> * * <p>This should be a singleton associated with application's main process. @@ -71,8 +69,8 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco private static final boolean DEBUG = false; private static final String HANDLER_THREAD_NAME = "RecordingScheduler"; - private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1); - @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30); + private static final long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1); + @VisibleForTesting static final long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30); private final Looper mLooper; private final InputSessionManager mSessionManager; @@ -98,21 +96,22 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco } }; - private Listener mChannelDataLoadListener = new Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - if (isDbLoaded()) { - updateInternal(); - } - } + private Listener mChannelDataLoadListener = + new Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + if (isDbLoaded()) { + updateInternal(); + } + } - @Override - public void onChannelListUpdated() { } + @Override + public void onChannelListUpdated() {} - @Override - public void onChannelBrowsableChanged() { } - }; + @Override + public void onChannelBrowsableChanged() {} + }; /** * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks. @@ -120,21 +119,32 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco */ public static RecordingScheduler createScheduler(Context context) { SoftPreconditions.checkState( - TvApplication.getSingletons(context).getRecordingScheduler() == null); + TvSingletons.getSingletons(context).getRecordingScheduler() == null); HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME); handlerThread.start(); - ApplicationSingletons singletons = TvApplication.getSingletons(context); - return new RecordingScheduler(handlerThread.getLooper(), - singletons.getDvrManager(), singletons.getInputSessionManager(), + TvSingletons singletons = TvSingletons.getSingletons(context); + return new RecordingScheduler( + handlerThread.getLooper(), + singletons.getDvrManager(), + singletons.getInputSessionManager(), (WritableDvrDataManager) singletons.getDvrDataManager(), - singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), context, - Clock.SYSTEM, (AlarmManager) context.getSystemService(Context.ALARM_SERVICE)); + singletons.getChannelDataManager(), + singletons.getTvInputManagerHelper(), + context, + Clock.SYSTEM, + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE)); } @VisibleForTesting - RecordingScheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, - TvInputManagerHelper inputManager, Context context, Clock clock, + RecordingScheduler( + Looper looper, + DvrManager dvrManager, + InputSessionManager sessionManager, + WritableDvrDataManager dataManager, + ChannelDataManager channelDataManager, + TvInputManagerHelper inputManager, + Context context, + Clock clock, AlarmManager alarmManager) { mLooper = looper; mDvrManager = dvrManager; @@ -159,9 +169,7 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco } } - /** - * Start recording that will happen soon, and set the next alarm time. - */ + /** Start recording that will happen soon, and set the next alarm time. */ public void updateAndStartServiceIfNeeded() { if (DEBUG) Log.d(TAG, "update and start service if needed"); if (isDbLoaded()) { @@ -185,8 +193,10 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco } private boolean updatePendingRecordings() { - List<ScheduledRecording> scheduledRecordings = mDataManager - .getScheduledRecordings(new Range<>(mLastStartTimePendingMs, + List<ScheduledRecording> scheduledRecordings = + mDataManager.getScheduledRecordings( + new Range<>( + mLastStartTimePendingMs, mClock.currentTimeMillis() + SOON_DURATION_IN_MS), ScheduledRecording.STATE_RECORDING_NOT_STARTED); for (ScheduledRecording r : scheduledRecordings) { @@ -198,7 +208,8 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco // recording service being wrongly pushed back to background in updateInternal(). return scheduledRecordings.size() > 0 || (mLastStartTimePendingMs > mClock.currentTimeMillis() - && mLastStartTimePendingMs < mClock.currentTimeMillis() + SOON_DURATION_IN_MS); + && mLastStartTimePendingMs + < mClock.currentTimeMillis() + SOON_DURATION_IN_MS); } private boolean isDbLoaded() { @@ -269,18 +280,32 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); if (input == null) { Log.e(TAG, "Can't find input for " + schedule); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); + mDataManager.changeState( + schedule, + ScheduledRecording.STATE_RECORDING_FAILED, + ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE); return; } if (!input.canRecord() || input.getTunerCount() <= 0) { Log.e(TAG, "TV input doesn't support recording: " + input); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); + mDataManager.changeState( + schedule, + ScheduledRecording.STATE_RECORDING_FAILED, + ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED); return; } InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId()); if (inputTaskScheduler == null) { - inputTaskScheduler = new InputTaskScheduler(mContext, input, mLooper, - mChannelDataManager, mDvrManager, mDataManager, mSessionManager, mClock); + inputTaskScheduler = + new InputTaskScheduler( + mContext, + input, + mLooper, + mChannelDataManager, + mDvrManager, + mDataManager, + mSessionManager, + mClock); mInputSchedulerMap.put(input.getId(), inputTaskScheduler); } inputTaskScheduler.addSchedule(schedule); @@ -290,8 +315,9 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco } private void updateNextAlarm() { - long nextStartTime = mDataManager.getNextScheduledStartTimeAfter( - Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); + long nextStartTime = + mDataManager.getNextScheduledStartTimeAfter( + Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java index 14888056..07a29e51 100644 --- a/src/com/android/tv/dvr/recorder/RecordingTask.java +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -26,24 +26,24 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; import android.widget.Toast; - import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.RecordingSession; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.common.util.Clock; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; -import com.android.tv.util.Clock; import com.android.tv.util.Utils; - import java.util.Comparator; import java.util.concurrent.TimeUnit; @@ -55,58 +55,45 @@ import java.util.concurrent.TimeUnit; */ @WorkerThread @TargetApi(Build.VERSION_CODES.N) -public class RecordingTask extends RecordingCallback implements Handler.Callback, - DvrManager.Listener { +public class RecordingTask extends RecordingCallback + implements Handler.Callback, DvrManager.Listener { private static final String TAG = "RecordingTask"; private static final boolean DEBUG = false; - /** - * Compares the end time in ascending order. - */ - public static final Comparator<RecordingTask> END_TIME_COMPARATOR - = new Comparator<RecordingTask>() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); - } - }; - - /** - * Compares ID in ascending order. - */ - public static final Comparator<RecordingTask> ID_COMPARATOR - = new Comparator<RecordingTask>() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); - } - }; - - /** - * Compares the priority in ascending order. - */ - public static final Comparator<RecordingTask> PRIORITY_COMPARATOR - = new Comparator<RecordingTask>() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getPriority(), rhs.getPriority()); - } - }; + /** Compares the end time in ascending order. */ + public static final Comparator<RecordingTask> END_TIME_COMPARATOR = + new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); + } + }; + + /** Compares ID in ascending order. */ + public static final Comparator<RecordingTask> ID_COMPARATOR = + new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); + } + }; + + /** Compares the priority in ascending order. */ + public static final Comparator<RecordingTask> PRIORITY_COMPARATOR = + new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getPriority(), rhs.getPriority()); + } + }; - @VisibleForTesting - static final int MSG_INITIALIZE = 1; - @VisibleForTesting - static final int MSG_START_RECORDING = 2; - @VisibleForTesting - static final int MSG_STOP_RECORDING = 3; - /** - * Message to update schedule. - */ + @VisibleForTesting static final int MSG_INITIALIZE = 1; + @VisibleForTesting static final int MSG_START_RECORDING = 2; + @VisibleForTesting static final int MSG_STOP_RECORDING = 3; + /** Message to update schedule. */ public static final int MSG_UDPATE_SCHEDULE = 4; - /** - * The time when the start command will be sent before the recording starts. - */ + /** The time when the start command will be sent before the recording starts. */ public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3); /** * If the recording starts later than the scheduled start time or ends before the scheduled end @@ -126,6 +113,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback ERROR, RELEASED, } + private final InputSessionManager mSessionManager; private final DvrManager mDvrManager; private final Context mContext; @@ -142,9 +130,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private Uri mRecordedProgramUri; private boolean mCanceled; - RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock) { + RecordingTask( + Context context, + ScheduledRecording scheduledRecording, + Channel channel, + DvrManager dvrManager, + InputSessionManager sessionManager, + WritableDvrDataManager dataManager, + Clock clock) { mContext = context; mScheduledRecording = scheduledRecording; mChannel = channel; @@ -163,8 +156,10 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback @Override public boolean handleMessage(Message msg) { if (DEBUG) Log.d(TAG, "handleMessage " + msg); - SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, - TAG, "Null handler trying to handle " + msg); + SoftPreconditions.checkState( + msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, + TAG, + "Null handler trying to handle " + msg); try { switch (msg.what) { case MSG_INITIALIZE: @@ -185,7 +180,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback release(); return false; default: - SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); + SoftPreconditions.checkArgument(false, TAG, "unexpected message type %s", msg); break; } return true; @@ -200,7 +195,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback public void onDisconnected(String inputId) { if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); if (mRecordingSession != null && mState != State.FINISHED) { - failAndQuit(); + failAndQuit(ScheduledRecording.FAILED_REASON_NOT_FINISHED); } } @@ -208,7 +203,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback public void onConnectionFailed(String inputId) { if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); if (mRecordingSession != null) { - failAndQuit(); + failAndQuit(ScheduledRecording.FAILED_REASON_CONNECTION_FAILED); } } @@ -219,23 +214,27 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback return; } mState = State.CONNECTED; - if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, - mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { - failAndQuit(); + if (mHandler == null + || !sendEmptyMessageAtAbsoluteTime( + MSG_START_RECORDING, + mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { + failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT); } } @Override public void onRecordingStopped(Uri recordedProgramUri) { - if (DEBUG) Log.d(TAG, "onRecordingStopped"); + Log.i(TAG, "Recording Stopped: " + mScheduledRecording); + Log.i(TAG, "Recording Stopped: stored as " + recordedProgramUri); if (mRecordingSession == null) { return; } mRecordedProgramUri = recordedProgramUri; mState = State.FINISHED; int state = ScheduledRecording.STATE_RECORDING_FINISHED; - if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS - > mClock.currentTimeMillis()) { + if (mStartedWithClipping + || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS + > mClock.currentTimeMillis()) { state = ScheduledRecording.STATE_RECORDING_CLIPPED; } updateRecordingState(state); @@ -247,65 +246,89 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback @Override public void onError(int reason) { - if (DEBUG) Log.d(TAG, "onError reason " + reason); + Log.i(TAG, "Recording failed with code=" + reason + " for " + mScheduledRecording); if (mRecordingSession == null) { return; } + int error; switch (reason) { case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE: - mMainThreadHandler.post(new Runnable() { - @Override - public void run() { - if (TvApplication.getSingletons(mContext).getMainActivityWrapper() - .isResumed()) { - ScheduledRecording scheduledRecording = mDataManager - .getScheduledRecording(mScheduledRecording.getId()); - if (scheduledRecording != null) { - Toast.makeText(mContext.getApplicationContext(), - mContext.getString(R.string - .dvr_error_insufficient_space_description_one_recording, - scheduledRecording.getProgramDisplayTitle(mContext)), - Toast.LENGTH_LONG) - .show(); + Log.i(TAG, "Insufficient space to record " + mScheduledRecording); + mMainThreadHandler.post( + new Runnable() { + @Override + public void run() { + if (TvSingletons.getSingletons(mContext) + .getMainActivityWrapper() + .isResumed()) { + ScheduledRecording scheduledRecording = + mDataManager.getScheduledRecording( + mScheduledRecording.getId()); + if (scheduledRecording != null) { + Toast.makeText( + mContext.getApplicationContext(), + mContext.getString( + R.string + .dvr_error_insufficient_space_description_one_recording, + scheduledRecording + .getProgramDisplayTitle( + mContext)), + Toast.LENGTH_LONG) + .show(); + } + } else { + Utils.setRecordingFailedReason( + mContext.getApplicationContext(), + TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.addFailedScheduledRecordingInfo( + mContext.getApplicationContext(), + mScheduledRecording.getProgramDisplayTitle(mContext)); + } } - } else { - Utils.setRecordingFailedReason(mContext.getApplicationContext(), - TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), - mScheduledRecording.getProgramDisplayTitle(mContext)); - } - } - }); - // Pass through + }); + error = ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE; + break; + case TvInputManager.RECORDING_ERROR_RESOURCE_BUSY: + error = ScheduledRecording.FAILED_REASON_RESOURCE_BUSY; + break; default: - failAndQuit(); + error = ScheduledRecording.FAILED_REASON_OTHER; break; } + failAndQuit(error); } private void handleInit() { if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { Log.w(TAG, "End time already past, not recording " + mScheduledRecording); - failAndQuit(); + failAndQuit(ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED); return; } if (mChannel == null) { Log.w(TAG, "Null channel for " + mScheduledRecording); - failAndQuit(); + failAndQuit(ScheduledRecording.FAILED_REASON_INVALID_CHANNEL); return; } if (mChannel.getId() != mScheduledRecording.getChannelId()) { - Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " - + mScheduledRecording); - failAndQuit(); + Log.w( + TAG, + "Channel" + + mChannel + + " does not match scheduled recording " + + mScheduledRecording); + failAndQuit(ScheduledRecording.FAILED_REASON_INVALID_CHANNEL); return; } String inputId = mChannel.getInputId(); - mRecordingSession = mSessionManager.createRecordingSession(inputId, - "recordingTask-" + mScheduledRecording.getId(), this, - mHandler, mScheduledRecording.getEndTimeMs()); + mRecordingSession = + mSessionManager.createRecordingSession( + inputId, + "recordingTask-" + mScheduledRecording.getId(), + this, + mHandler, + mScheduledRecording.getEndTimeMs()); mState = State.SESSION_ACQUIRED; mDvrManager.addListener(this, mHandler); mRecordingSession.tune(inputId, mChannel.getUri()); @@ -313,8 +336,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } private void failAndQuit() { + failAndQuit(ScheduledRecording.FAILED_REASON_OTHER); + } + + private void failAndQuit(Integer reason) { if (DEBUG) Log.d(TAG, "failAndQuit"); - updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + updateRecordingState( + ScheduledRecording.STATE_RECORDING_FAILED, + reason); mState = State.ERROR; sendRemove(); } @@ -322,16 +351,18 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private void sendRemove() { if (DEBUG) Log.d(TAG, "sendRemove"); if (mHandler != null) { - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( - HandlerWrapper.MESSAGE_REMOVE)); + mHandler.sendMessageAtFrontOfQueue( + mHandler.obtainMessage(HandlerWrapper.MESSAGE_REMOVE)); } } private void handleStartRecording() { - if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); + Log.i(TAG, "Start Recording: " + mScheduledRecording); long programId = mScheduledRecording.getProgramId(); - mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null - : TvContract.buildProgramUri(programId)); + mRecordingSession.startRecording( + programId == ScheduledRecording.ID_NOT_SET + ? null + : TvContract.buildProgramUri(programId)); updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS); // If it starts late, it's clipped. if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS @@ -340,14 +371,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } mState = State.RECORDING_STARTED; - if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, - mScheduledRecording.getEndTimeMs())) { - failAndQuit(); + if (!sendEmptyMessageAtAbsoluteTime( + MSG_STOP_RECORDING, mScheduledRecording.getEndTimeMs())) { + failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT); } } private void handleStopRecording() { - if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); + Log.i(TAG, "Stop Recording: " + mScheduledRecording); mRecordingSession.stopRecording(); mState = State.RECORDING_STOP_REQUESTED; } @@ -362,7 +393,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback if (mState == State.RECORDING_STARTED) { mHandler.removeMessages(MSG_STOP_RECORDING); if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { - failAndQuit(); + failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT); } } } @@ -377,23 +408,17 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback return mScheduledRecording.getId(); } - /** - * Returns the priority. - */ + /** Returns the priority. */ public long getPriority() { return mScheduledRecording.getPriority(); } - /** - * Returns the start time of the recording. - */ + /** Returns the start time of the recording. */ public long getStartTimeMs() { return mScheduledRecording.getStartTimeMs(); } - /** - * Returns the end time of the recording. - */ + /** Returns the end time of the recording. */ public long getEndTimeMs() { return mScheduledRecording.getEndTimeMs(); } @@ -410,33 +435,53 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback long now = mClock.currentTimeMillis(); long delay = Math.max(0L, when - now); if (DEBUG) { - Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 - + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); + Log.d( + TAG, + "Sending message " + + what + + " with a delay of " + + delay / 1000 + + " seconds to arrive at " + + CommonUtils.toIsoDateTimeString(when)); } return mHandler.sendEmptyMessageDelayed(what, delay); } private void updateRecordingState(@ScheduledRecording.RecordingState int state) { - if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); - mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) - .build(); - runOnMainThread(new Runnable() { - @Override - public void run() { - ScheduledRecording schedule = mDataManager.getScheduledRecording( - mScheduledRecording.getId()); - if (schedule == null) { - // Schedule has been deleted. Delete the recorded program. - removeRecordedProgram(); - } else { - // Update the state based on the object in DataManager in case when it has been - // updated. mScheduledRecording will be updated from - // onScheduledRecordingStateChanged. - mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) - .setState(state).build()); - } - } - }); + updateRecordingState(state, null); + } + private void updateRecordingState( + @ScheduledRecording.RecordingState int state, @Nullable Integer reason) { + if (DEBUG) { + Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); + } + mScheduledRecording = + ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build(); + runOnMainThread( + new Runnable() { + @Override + public void run() { + ScheduledRecording schedule = + mDataManager.getScheduledRecording(mScheduledRecording.getId()); + if (schedule == null) { + // Schedule has been deleted. Delete the recorded program. + removeRecordedProgram(); + } else { + // Update the state based on the object in DataManager in case when it + // has been updated. mScheduledRecording will be updated from + // onScheduledRecordingStateChanged. + ScheduledRecording.Builder builder = + ScheduledRecording + .buildFrom(schedule) + .setState(state); + if (state == ScheduledRecording.STATE_RECORDING_FAILED + && reason != null) { + builder.setFailedReason(reason); + } + mDataManager.updateScheduledRecording(builder.build()); + } + } + }); } @Override @@ -447,16 +492,12 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback stop(); } - /** - * Starts the task. - */ + /** Starts the task. */ public void start() { mHandler.sendEmptyMessage(MSG_INITIALIZE); } - /** - * Stops the task. - */ + /** Stops the task. */ public void stop() { if (DEBUG) Log.d(TAG, "stop"); switch (mState) { @@ -480,9 +521,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } } - /** - * Cancels the task - */ + /** Cancels the task */ public void cancel() { if (DEBUG) Log.d(TAG, "cancel"); mCanceled = true; @@ -490,12 +529,12 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback removeRecordedProgram(); } - /** - * Clean up the task. - */ + /** Clean up the task. */ public void cleanUp() { if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { - updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + updateRecordingState( + ScheduledRecording.STATE_RECORDING_FAILED, + ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED); } release(); if (mHandler != null) { @@ -509,14 +548,15 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } private void removeRecordedProgram() { - runOnMainThread(new Runnable() { - @Override - public void run() { - if (mRecordedProgramUri != null) { - mDvrManager.removeRecordedProgram(mRecordedProgramUri); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + if (mRecordedProgramUri != null) { + mDvrManager.removeRecordedProgram(mRecordedProgramUri); + } + } + }); } private void runOnMainThread(Runnable runnable) { diff --git a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java index d958c4a1..dd106e1c 100644 --- a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java +++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java @@ -17,24 +17,18 @@ package com.android.tv.dvr.recorder; import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; - +import com.android.tv.common.util.Clock; import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; -import com.android.tv.util.Clock; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -/** - * Deletes {@link ScheduledRecording} older than {@value @DAYS} days. - */ -class ScheduledProgramReaper implements Runnable { +/** Deletes {@link ScheduledRecording} older than {@value @DAYS} days. */ +public class ScheduledProgramReaper implements Runnable { - @VisibleForTesting - static final int DAYS = 2; + public static final int DAYS = 7; private final WritableDvrDataManager mDvrDataManager; private final Clock mClock; @@ -54,7 +48,7 @@ class ScheduledProgramReaper implements Runnable { // series recording. if (r.getEndTimeMs() < cutoff && (r.getSeriesRecordingId() == SeriesRecording.ID_NOT_SET - || r.getState() != ScheduledRecording.STATE_RECORDING_FINISHED)) { + || r.getState() != ScheduledRecording.STATE_RECORDING_FINISHED)) { toRemove.add(r); } } diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java index 15508c24..4f7a789b 100644 --- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java @@ -27,27 +27,23 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.LongSparseArray; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.common.CollectionUtils; -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.experiments.Experiments; +import com.android.tv.common.util.CollectionUtils; +import com.android.tv.common.util.SharedPreferencesUtils; import com.android.tv.data.Program; -import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.data.epg.EpgReader; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.WritableDvrDataManager; -import com.android.tv.dvr.data.SeasonEpisodeNumber; import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeasonEpisodeNumber; import com.android.tv.dvr.data.SeriesInfo; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.EpisodicProgramLoadTask; -import com.android.tv.experiments.Experiments; - -import com.android.tv.util.LocationUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -60,12 +56,14 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import javax.inject.Provider; /** - * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for - * the {@link com.android.tv.dvr.data.SeriesRecording}. - * <p> - * The current implementation assumes that the series recordings are scheduled only for one channel. + * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for the {@link + * com.android.tv.dvr.data.SeriesRecording}. + * + * <p>The current implementation assumes that the series recordings are scheduled only for one + * channel. */ @TargetApi(Build.VERSION_CODES.N) public class SeriesRecordingScheduler { @@ -78,9 +76,7 @@ public class SeriesRecordingScheduler { @SuppressLint("StaticFieldLeak") private static SeriesRecordingScheduler sInstance; - /** - * Creates and returns the {@link SeriesRecordingScheduler}. - */ + /** Creates and returns the {@link SeriesRecordingScheduler}. */ public static synchronized SeriesRecordingScheduler getInstance(Context context) { if (sInstance == null) { sInstance = new SeriesRecordingScheduler(context); @@ -100,54 +96,59 @@ public class SeriesRecordingScheduler { private boolean mPaused; private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); - private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - executeFetchSeriesInfoTask(seriesRecording); - } - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - // Cancel the update. - for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { - SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, - SeriesRecording.ID_COMPARATOR).isEmpty()) { - task.cancel(true); - iter.remove(); + private final SeriesRecordingListener mSeriesRecordingListener = + new SeriesRecordingListener() { + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + executeFetchSeriesInfoTask(seriesRecording); + } } - } - for (SeriesRecording seriesRecording : seriesRecordings) { - FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId()); - if (task != null) { - task.cancel(true); - mFetchSeriesInfoTasks.remove(seriesRecording.getId()); + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + // Cancel the update. + for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); + iter.hasNext(); ) { + SeriesRecordingUpdateTask task = iter.next(); + if (CollectionUtils.subtract( + task.getSeriesRecordings(), + seriesRecordings, + SeriesRecording.ID_COMPARATOR) + .isEmpty()) { + task.cancel(true); + iter.remove(); + } + } + for (SeriesRecording seriesRecording : seriesRecordings) { + FetchSeriesInfoTask task = + mFetchSeriesInfoTasks.get(seriesRecording.getId()); + if (task != null) { + task.cancel(true); + mFetchSeriesInfoTasks.remove(seriesRecording.getId()); + } + } } - } - } - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - List<SeriesRecording> stopped = new ArrayList<>(); - List<SeriesRecording> normal = new ArrayList<>(); - for (SeriesRecording r : seriesRecordings) { - if (r.isStopped()) { - stopped.add(r); - } else { - normal.add(r); + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + List<SeriesRecording> stopped = new ArrayList<>(); + List<SeriesRecording> normal = new ArrayList<>(); + for (SeriesRecording r : seriesRecordings) { + if (r.isStopped()) { + stopped.add(r); + } else { + normal.add(r); + } + } + if (!stopped.isEmpty()) { + onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); + } + if (!normal.isEmpty()) { + updateSchedules(normal); + } } - } - if (!stopped.isEmpty()) { - onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); - } - if (!normal.isEmpty()) { - updateSchedules(normal); - } - } - }; + }; private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @@ -166,7 +167,8 @@ public class SeriesRecordingScheduler { List<ScheduledRecording> schedulesForUpdate = new ArrayList<>(); for (ScheduledRecording r : schedules) { if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED - || r.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED) + || r.getState() + == ScheduledRecording.STATE_RECORDING_CLIPPED) && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET && !TextUtils.isEmpty(r.getSeasonNumber()) && !TextUtils.isEmpty(r.getEpisodeNumber())) { @@ -205,18 +207,17 @@ public class SeriesRecordingScheduler { private SeriesRecordingScheduler(Context context) { mContext = context.getApplicationContext(); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mDvrManager = appSingletons.getDvrManager(); - mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); - mSharedPreferences = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); - mFetchedSeriesIds.addAll(mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, - Collections.emptySet())); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mDvrManager = tvSingletons.getDvrManager(); + mDataManager = (WritableDvrDataManager) tvSingletons.getDvrDataManager(); + mSharedPreferences = + context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); + mFetchedSeriesIds.addAll( + mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, Collections.emptySet())); } - /** - * Starts the scheduler. - */ + /** Starts the scheduler. */ @MainThread public void start() { SoftPreconditions.checkState(mDataManager.isInitialized()); @@ -261,15 +262,16 @@ public class SeriesRecordingScheduler { private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) { if (Experiments.CLOUD_EPG.get()) { - FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); + FetchSeriesInfoTask task = + new FetchSeriesInfoTask( + seriesRecording, + TvSingletons.getSingletons(mContext).providesEpgReader()); task.execute(); mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); } } - /** - * Pauses the updates of the series recordings. - */ + /** Pauses the updates of the series recordings. */ public void pauseUpdate() { if (DEBUG) Log.d(TAG, "Schedule paused"); if (mPaused) { @@ -287,9 +289,7 @@ public class SeriesRecordingScheduler { } } - /** - * Resumes the updates of the series recordings. - */ + /** Resumes the updates of the series recordings. */ public void resumeUpdate() { if (DEBUG) Log.d(TAG, "Schedule resumed"); if (!mPaused) { @@ -329,25 +329,28 @@ public class SeriesRecordingScheduler { mPendingSeriesRecordings.add(r.getId()); } if (DEBUG) { - Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size=" - + mPendingSeriesRecordings.size()); + Log.d( + TAG, + "The scheduler has been paused. Adding to the pending list. size=" + + mPendingSeriesRecordings.size()); } return; } Set<SeriesRecording> previousSeriesRecordings = new HashSet<>(); for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { + iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings, - SeriesRecording.ID_COMPARATOR)) { + if (CollectionUtils.containsAny( + task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR)) { // The task is affected by the seriesRecordings task.cancel(true); previousSeriesRecordings.addAll(task.getSeriesRecordings()); iter.remove(); } } - List<SeriesRecording> seriesRecordingsToUpdate = CollectionUtils.union(seriesRecordings, - previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); + List<SeriesRecording> seriesRecordingsToUpdate = + CollectionUtils.union( + seriesRecordings, previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator(); iter.hasNext(); ) { SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); @@ -367,8 +370,8 @@ public class SeriesRecordingScheduler { task.execute(); } else { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { - SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask( - Collections.singletonList(seriesRecording)); + SeriesRecordingUpdateTask task = + new SeriesRecordingUpdateTask(Collections.singletonList(seriesRecording)); mScheduleTasks.add(task); if (DEBUG) Log.d(TAG, "Added schedule task: " + task); task.execute(); @@ -389,8 +392,9 @@ public class SeriesRecordingScheduler { * Pick one program per an episode. * * <p>Note that the programs which has been already scheduled have the highest priority, and all - * of them are added even though they are the same episodes. That's because the schedules - * should be added to the series recording. + * of them are added even though they are the same episodes. That's because the schedules should + * be added to the series recording. + * * <p>If there are no existing schedules for an episode, one program which starts earlier is * picked. */ @@ -399,11 +403,10 @@ public class SeriesRecordingScheduler { return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); } - /** - * @see #pickOneProgramPerEpisode(List, List) - */ + /** @see #pickOneProgramPerEpisode(List, List) */ public static LongSparseArray<List<Program>> pickOneProgramPerEpisode( - DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, + DvrDataManager dataManager, + List<SeriesRecording> seriesRecordings, List<Program> programs) { // Initialize. LongSparseArray<List<Program>> result = new LongSparseArray<>(); @@ -422,8 +425,11 @@ public class SeriesRecordingScheduler { result.get(seriesRecordingId).add(program); continue; } - SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, - program.getSeasonNumber(), program.getEpisodeNumber()); + SeasonEpisodeNumber seasonEpisodeNumber = + new SeasonEpisodeNumber( + seriesRecordingId, + program.getSeasonNumber(), + program.getEpisodeNumber()); List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); if (programsForEpisode == null) { programsForEpisode = new ArrayList<>(); @@ -434,22 +440,24 @@ public class SeriesRecordingScheduler { // Pick one program. for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) { List<Program> programsForEpisode = entry.getValue(); - Collections.sort(programsForEpisode, new Comparator<Program>() { - @Override - public int compare(Program lhs, Program rhs) { - // Place the existing schedule first. - boolean lhsScheduled = isProgramScheduled(dataManager, lhs); - boolean rhsScheduled = isProgramScheduled(dataManager, rhs); - if (lhsScheduled && !rhsScheduled) { - return -1; - } - if (!lhsScheduled && rhsScheduled) { - return 1; - } - // Sort by the start time in ascending order. - return lhs.compareTo(rhs); - } - }); + Collections.sort( + programsForEpisode, + new Comparator<Program>() { + @Override + public int compare(Program lhs, Program rhs) { + // Place the existing schedule first. + boolean lhsScheduled = isProgramScheduled(dataManager, lhs); + boolean rhsScheduled = isProgramScheduled(dataManager, rhs); + if (lhsScheduled && !rhsScheduled) { + return -1; + } + if (!lhsScheduled && rhsScheduled) { + return 1; + } + // Sort by the start time in ascending order. + return lhs.compareTo(rhs); + } + }); boolean added = false; // Add all the scheduled programs List<Program> programsForSeries = result.get(entry.getKey().seriesRecordingId); @@ -469,8 +477,8 @@ public class SeriesRecordingScheduler { private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) { ScheduledRecording schedule = dataManager.getScheduledRecordingForProgramId(program.getId()); - return schedule != null && schedule.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + return schedule != null + && schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; } private void updateFetchedSeries() { @@ -478,8 +486,8 @@ public class SeriesRecordingScheduler { } /** - * This works only for the existing series recordings. Do not use this task for the - * "adding series recording" UI. + * This works only for the existing series recordings. Do not use this task for the "adding + * series recording" UI. */ private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) { @@ -491,16 +499,17 @@ public class SeriesRecordingScheduler { if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); mScheduleTasks.remove(this); if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " - + getSeriesRecordings()); + Log.e( + TAG, + "Creating schedules for series recording failed: " + getSeriesRecordings()); return; } - LongSparseArray<List<Program>> seriesProgramMap = pickOneProgramPerEpisode( - getSeriesRecordings(), programs); + LongSparseArray<List<Program>> seriesProgramMap = + pickOneProgramPerEpisode(getSeriesRecordings(), programs); for (SeriesRecording seriesRecording : getSeriesRecordings()) { // Check the series recording is still valid. - SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( - seriesRecording.getId()); + SeriesRecording actualSeriesRecording = + mDataManager.getSeriesRecording(seriesRecording.getId()); if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { continue; } @@ -520,35 +529,39 @@ public class SeriesRecordingScheduler { @Override public String toString() { return "SeriesRecordingUpdateTask:{" - + "series_recordings=" + getSeriesRecordings() + + "series_recordings=" + + getSeriesRecordings() + "}"; } } private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> { - private SeriesRecording mSeriesRecording; + private final SeriesRecording mSeriesRecording; + private final Provider<EpgReader> mEpgReaderProvider; - FetchSeriesInfoTask(SeriesRecording seriesRecording) { + FetchSeriesInfoTask( + SeriesRecording seriesRecording, Provider<EpgReader> epgReaderProvider) { mSeriesRecording = seriesRecording; + mEpgReaderProvider = epgReaderProvider; } @Override protected SeriesInfo doInBackground(Void... voids) { - return EpgFetcher.createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext)) - .getSeriesInfo(mSeriesRecording.getSeriesId()); + return mEpgReaderProvider.get().getSeriesInfo(mSeriesRecording.getSeriesId()); } @Override protected void onPostExecute(SeriesInfo seriesInfo) { if (seriesInfo != null) { - mDataManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) - .setTitle(seriesInfo.getTitle()) - .setDescription(seriesInfo.getDescription()) - .setLongDescription(seriesInfo.getLongDescription()) - .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) - .setPosterUri(seriesInfo.getPosterUri()) - .setPhotoUri(seriesInfo.getPhotoUri()) - .build()); + mDataManager.updateSeriesRecording( + SeriesRecording.buildFrom(mSeriesRecording) + .setTitle(seriesInfo.getTitle()) + .setDescription(seriesInfo.getDescription()) + .setLongDescription(seriesInfo.getLongDescription()) + .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) + .setPosterUri(seriesInfo.getPosterUri()) + .setPhotoUri(seriesInfo.getPhotoUri()) + .build()); mFetchedSeriesIds.add(seriesInfo.getId()); updateFetchedSeries(); } diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java index ec3b5065..0d6ff8b1 100644 --- a/src/com/android/tv/dvr/ui/BigArguments.java +++ b/src/com/android/tv/dvr/ui/BigArguments.java @@ -17,37 +17,27 @@ package com.android.tv.dvr.ui; import android.support.annotation.NonNull; - import com.android.tv.common.SoftPreconditions; - import java.util.HashMap; import java.util.Map; -/** - * Stores the object to pass through activities/fragments. - */ +/** Stores the object to pass through activities/fragments. */ public class BigArguments { - private final static String TAG = "BigArguments"; + private static final String TAG = "BigArguments"; private static Map<String, Object> sBigArgumentMap = new HashMap<>(); - /** - * Sets the argument. - */ + /** Sets the argument. */ public static void setArgument(String name, @NonNull Object value) { SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null"); sBigArgumentMap.put(name, value); } - /** - * Returns the argument which is associated to the name. - */ + /** Returns the argument which is associated to the name. */ public static Object getArgument(String name) { return sBigArgumentMap.get(name); } - /** - * Resets the arguments. - */ + /** Resets the arguments. */ public static void reset() { sBigArgumentMap.clear(); } diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java index cddece73..32679421 100644 --- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -26,16 +26,14 @@ import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; - import com.android.tv.R; - import java.util.Map; /** - * TODO: Remove this class once b/32405620 is fixed. - * This class is for the workaround of b/32405620 and only for the shared element transition between - * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and - * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}. + * TODO: Remove this class once b/32405620 is fixed. This class is for the workaround of b/32405620 + * and only for the shared element transition between {@link + * com.android.tv.dvr.ui.browse.RecordingCardView} and {@link + * com.android.tv.dvr.ui.browse.DvrDetailsActivity}. */ public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; @@ -60,7 +58,8 @@ public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { View view = transitionValues.view; Map<String, Object> values = transitionValues.values; Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX); - if (matrix != null && view.getId() == R.id.details_overview_image + if (matrix != null + && view.getId() == R.id.details_overview_image && view instanceof ImageView) { ImageView imageView = (ImageView) view; if (imageView.getScaleType() == ScaleType.CENTER_INSIDE @@ -68,10 +67,13 @@ public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); if (bitmap.getWidth() < imageView.getWidth() && bitmap.getHeight() < imageView.getHeight()) { - float scale = imageView.getContext().getResources().getFraction( - R.fraction.lb_focus_zoom_factor_medium, 1, 1); - matrix.postScale(scale, scale, imageView.getWidth() / 2, - imageView.getHeight() / 2); + float scale = + imageView + .getContext() + .getResources() + .getFraction(R.fraction.lb_focus_zoom_factor_medium, 1, 1); + matrix.postScale( + scale, scale, imageView.getWidth() / 2, imageView.getHeight() / 2); } } } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index 62327870..fce94230 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -24,13 +24,11 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.RecordedProgram; - import java.util.List; /** @@ -51,13 +49,19 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { public void onAttach(Context context) { super.onAttach(context); mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); - DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager(); - mDuplicate = dvrManager.getRecordedProgram(mProgram.getTitle(), - mProgram.getSeasonNumber(), mProgram.getEpisodeNumber()); + DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager(); + mDuplicate = + dvrManager.getRecordedProgram( + mProgram.getTitle(), + mProgram.getSeasonNumber(), + mProgram.getEpisodeNumber()); if (mDuplicate == null) { dvrManager.addSchedule(mProgram); - DvrUiHelper.showAddScheduleToast(context, mProgram.getTitle(), - mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis()); + DvrUiHelper.showAddScheduleToast( + context, + mProgram.getTitle(), + mProgram.getStartTimeUtcMillis(), + mProgram.getEndTimeUtcMillis()); dismissDialog(); } } @@ -74,18 +78,21 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { Context context = getContext(); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_RECORD_ANYWAY) - .title(R.string.dvr_action_record_anyway) - .build()); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_WATCH) - .title(R.string.dvr_action_watch_now) - .build()); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_CANCEL) - .title(R.string.dvr_action_record_cancel) - .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_WATCH) + .title(R.string.dvr_action_watch_now) + .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_CANCEL) + .title(R.string.dvr_action_record_cancel) + .build()); } @Override diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 6da75e55..456ad830 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -25,13 +25,11 @@ import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; - import java.util.List; /** @@ -52,13 +50,19 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { public void onAttach(Context context) { super.onAttach(context); mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); - DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager(); - mDuplicate = dvrManager.getScheduledRecording(mProgram.getTitle(), - mProgram.getSeasonNumber(), mProgram.getEpisodeNumber()); + DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager(); + mDuplicate = + dvrManager.getScheduledRecording( + mProgram.getTitle(), + mProgram.getSeasonNumber(), + mProgram.getEpisodeNumber()); if (mDuplicate == null) { dvrManager.addSchedule(mProgram); - DvrUiHelper.showAddScheduleToast(context, mProgram.getTitle(), - mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis()); + DvrUiHelper.showAddScheduleToast( + context, + mProgram.getTitle(), + mProgram.getStartTimeUtcMillis(), + mProgram.getEndTimeUtcMillis()); dismissDialog(); } } @@ -67,9 +71,13 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.dvr_already_scheduled_dialog_title); - String description = getString(R.string.dvr_already_scheduled_dialog_description, - DateUtils.formatDateTime(getContext(), mDuplicate.getStartTimeMs(), - DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE)); + String description = + getString( + R.string.dvr_already_scheduled_dialog_description, + DateUtils.formatDateTime( + getContext(), + mDuplicate.getStartTimeMs(), + DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE)); Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); return new Guidance(title, description, null, image); } @@ -77,18 +85,21 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { Context context = getContext(); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_RECORD_ANYWAY) - .title(R.string.dvr_action_record_anyway) - .build()); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_RECORD_INSTEAD) - .title(R.string.dvr_action_record_instead) - .build()); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_CANCEL) - .title(R.string.dvr_action_record_cancel) - .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_RECORD_INSTEAD) + .title(R.string.dvr_action_record_instead) + .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_CANCEL) + .title(R.string.dvr_action_record_cancel) + .build()); } @Override diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index 36659412..6be35cb2 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -21,15 +21,13 @@ import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -43,8 +41,10 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen Bundle args = getArguments(); if (args != null) { long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID); - mChannel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(channelId); + mChannel = + TvSingletons.getSingletons(getContext()) + .getChannelDataManager() + .getChannel(channelId); } SoftPreconditions.checkArgument(mChannel != null); super.onCreate(savedInstanceState); @@ -66,32 +66,36 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen mDurations.add(TimeUnit.HOURS.toMillis(1)); mDurations.add(TimeUnit.HOURS.toMillis(3)); - actions.add(new GuidedAction.Builder(getContext()) - .id(++actionId) - .title(R.string.recording_start_dialog_10_min_duration) - .build()); - actions.add(new GuidedAction.Builder(getContext()) - .id(++actionId) - .title(R.string.recording_start_dialog_30_min_duration) - .build()); - actions.add(new GuidedAction.Builder(getContext()) - .id(++actionId) - .title(R.string.recording_start_dialog_1_hour_duration) - .build()); - actions.add(new GuidedAction.Builder(getContext()) - .id(++actionId) - .title(R.string.recording_start_dialog_3_hours_duration) - .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(++actionId) + .title(R.string.recording_start_dialog_10_min_duration) + .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(++actionId) + .title(R.string.recording_start_dialog_30_min_duration) + .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(++actionId) + .title(R.string.recording_start_dialog_1_hour_duration) + .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(++actionId) + .title(R.string.recording_start_dialog_3_hours_duration) + .build()); } @Override public void onTrackedGuidedActionClicked(GuidedAction action) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); long duration = mDurations.get((int) action.getId()); long startTimeMs = System.currentTimeMillis(); long endTimeMs = System.currentTimeMillis() + duration; - List<ScheduledRecording> conflicts = dvrManager.getConflictingSchedules( - mChannel.getId(), startTimeMs, endTimeMs); + List<ScheduledRecording> conflicts = + dvrManager.getConflictingSchedules(mChannel.getId(), startTimeMs, endTimeMs); dvrManager.addSchedule(mChannel, startTimeMs, endTimeMs); if (conflicts.isEmpty()) { dismissDialog(); @@ -102,8 +106,7 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen args.putLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS, startTimeMs); args.putLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS, endTimeMs); fragment.setArguments(args); - GuidedStepFragment.add(getFragmentManager(), fragment, - R.id.halfsized_dialog_host); + GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); } } diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index 6f362e68..65759555 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -27,18 +27,16 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.recorder.ConflictChecker; import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; -import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -72,15 +70,16 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getContext()) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); - actions.add(new GuidedAction.Builder(getContext()) - .id(ACTION_VIEW_SCHEDULES) - .title(R.string.dvr_action_view_schedules) - .build()); + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + actions.add( + new GuidedAction.Builder(getContext()) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(ACTION_VIEW_SCHEDULES) + .title(R.string.dvr_action_view_schedules) + .build()); } @Override @@ -114,33 +113,45 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } switch (titles.size()) { case 0: - Log.i(TAG, "Conflict has been resolved by any reason. Maybe input might have" - + " been deleted."); + Log.i( + TAG, + "Conflict has been resolved by any reason. Maybe input might have" + + " been deleted."); return null; case 1: - return getResources().getString( - R.string.dvr_program_conflict_dialog_description_1, titles.get(0)); + return getResources() + .getString( + R.string.dvr_program_conflict_dialog_description_1, titles.get(0)); case 2: - return getResources().getString( - R.string.dvr_program_conflict_dialog_description_2, titles.get(0), - titles.get(1)); + return getResources() + .getString( + R.string.dvr_program_conflict_dialog_description_2, + titles.get(0), + titles.get(1)); case 3: - return getResources().getString( - R.string.dvr_program_conflict_dialog_description_3, titles.get(0), - titles.get(1)); + return getResources() + .getString( + R.string.dvr_program_conflict_dialog_description_3, + titles.get(0), + titles.get(1)); default: - return getResources().getQuantityString( - R.plurals.dvr_program_conflict_dialog_description_many, - titles.size() - LISTED_PROGRAM_COUNT, titles.get(0), titles.get(1), - titles.size() - LISTED_PROGRAM_COUNT); + return getResources() + .getQuantityString( + R.plurals.dvr_program_conflict_dialog_description_many, + titles.size() - LISTED_PROGRAM_COUNT, + titles.get(0), + titles.get(1), + titles.size() - LISTED_PROGRAM_COUNT); } } @Nullable private String getScheduleTitle(ScheduledRecording schedule) { if (schedule.getType() == ScheduledRecording.TYPE_TIMED) { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(schedule.getChannelId()); + Channel channel = + TvSingletons.getSingletons(getContext()) + .getChannelDataManager() + .getChannel(schedule.getChannelId()); if (channel != null) { return channel.getDisplayName(); } else { @@ -151,14 +162,13 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } } - /** - * A fragment to show the program conflict. - */ + /** A fragment to show the program conflict. */ public static class DvrProgramConflictFragment extends DvrConflictFragment { private Program mProgram; + @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); @@ -168,8 +178,10 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { SoftPreconditions.checkNotNull(input); List<ScheduledRecording> conflicts = null; if (input != null) { - conflicts = TvApplication.getSingletons(getContext()).getDvrManager() - .getConflictingSchedules(mProgram); + conflicts = + TvSingletons.getSingletons(getContext()) + .getDvrManager() + .getConflictingSchedules(mProgram); } if (conflicts == null) { conflicts = Collections.emptyList(); @@ -185,8 +197,10 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getResources().getString(R.string.dvr_program_conflict_dialog_title); - String descriptionPrefix = getString( - R.string.dvr_program_conflict_dialog_description_prefix, mProgram.getTitle()); + String descriptionPrefix = + getString( + R.string.dvr_program_conflict_dialog_description_prefix, + mProgram.getTitle()); String description = getConflictDescription(); if (description == null) { dismissDialog(); @@ -201,21 +215,21 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } } - /** - * A fragment to show the channel recording conflict. - */ + /** A fragment to show the channel recording conflict. */ public static class DvrChannelRecordConflictFragment extends DvrConflictFragment { private Channel mChannel; private long mStartTimeMs; private long mEndTimeMs; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle args = getArguments(); long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID); - mChannel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(channelId); + mChannel = + TvSingletons.getSingletons(getContext()) + .getChannelDataManager() + .getChannel(channelId); SoftPreconditions.checkArgument(mChannel != null); TvInputInfo input = Utils.getTvInputInfoForChannelId(getContext(), mChannel.getId()); SoftPreconditions.checkNotNull(input); @@ -223,8 +237,11 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { if (input != null) { mStartTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS); mEndTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS); - conflicts = TvApplication.getSingletons(getContext()).getDvrManager() - .getConflictingSchedules(mChannel.getId(), mStartTimeMs, mEndTimeMs); + conflicts = + TvSingletons.getSingletons(getContext()) + .getDvrManager() + .getConflictingSchedules( + mChannel.getId(), mStartTimeMs, mEndTimeMs); } if (conflicts == null) { conflicts = Collections.emptyList(); @@ -240,9 +257,10 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getResources().getString(R.string.dvr_channel_conflict_dialog_title); - String descriptionPrefix = getString( - R.string.dvr_channel_conflict_dialog_description_prefix, - mChannel.getDisplayName()); + String descriptionPrefix = + getString( + R.string.dvr_channel_conflict_dialog_description_prefix, + mChannel.getDisplayName()); String description = getConflictDescription(); if (description == null) { dismissDialog(); @@ -259,16 +277,16 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { /** * A fragment to show the channel watching conflict. - * <p> - * This fragment is automatically closed when there are no upcoming conflicts. + * + * <p>This fragment is automatically closed when there are no upcoming conflicts. */ public static class DvrChannelWatchConflictFragment extends DvrConflictFragment implements OnUpcomingConflictChangeListener { private long mChannelId; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mChannelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID); @@ -298,24 +316,27 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { @NonNull @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString( - R.string.dvr_epg_channel_watch_conflict_dialog_title); - String description = getResources().getString( - R.string.dvr_epg_channel_watch_conflict_dialog_description); + String title = + getResources().getString(R.string.dvr_epg_channel_watch_conflict_dialog_title); + String description = + getResources() + .getString(R.string.dvr_epg_channel_watch_conflict_dialog_description); return new Guidance(title, description, null, null); } @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getContext()) - .id(ACTION_DELETE_CONFLICT) - .title(R.string.dvr_action_delete_schedule) - .build()); - actions.add(new GuidedAction.Builder(getContext()) - .id(ACTION_CANCEL) - .title(R.string.dvr_action_record_program) - .build()); + public void onCreateActions( + @NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + actions.add( + new GuidedAction.Builder(getContext()) + .id(ACTION_DELETE_CONFLICT) + .title(R.string.dvr_action_delete_schedule) + .build()); + actions.add( + new GuidedAction.Builder(getContext()) + .id(ACTION_CANCEL) + .title(R.string.dvr_action_record_program) + .build()); } @Override diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java new file mode 100644 index 00000000..677a6cbb --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.GuidedAction; +import com.android.tv.TvSingletons; +import com.android.tv.data.Program; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.util.Utils; +import java.util.List; + +/** + * A fragment which shows the formation of a program. + */ +public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment { + private static final long ACTION_ID_VIEW_SCHEDULE = 1; + private ScheduledRecording mScheduledRecording; + private Program mProgram; + + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + long startTime = mProgram.getStartTimeUtcMillis(); + // TODO(b/71717923): use R.string when the strings are finalized + StringBuilder description = new StringBuilder() + .append("This program will start at ") + .append(Utils.getDurationString(getContext(), startTime, startTime, false)); + if (mScheduledRecording != null) { + description.append("\nThis program has been scheduled for recording."); + } + return new GuidanceStylist.Guidance( + mProgram.getTitle(), description.toString(), null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); + mScheduledRecording = + TvSingletons.getSingletons(getContext()) + .getDvrDataManager() + .getScheduledRecordingForProgramId(mProgram.getId()); + actions.add( + new GuidedAction.Builder(activity) + .id(GuidedAction.ACTION_ID_OK) + .title(android.R.string.ok) + .build()); + if (mScheduledRecording != null) { + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_ID_VIEW_SCHEDULE) + .title("View schedules") + .build()); + } + + } + + @Override + public void onTrackedGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_ID_VIEW_SCHEDULE) { + DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording); + return; + } + dismissDialog(); + } + + @Override + public String getTrackerPrefix() { + return "DvrFutureProgramInfoFragment"; + } +} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java index 6b0c22ff..611962d0 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java @@ -24,12 +24,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; - import com.android.tv.R; -/** - * Stylist class used for DVR settings {@link GuidedStepFragment}. - */ +/** Stylist class used for DVR settings {@link GuidedStepFragment}. */ public class DvrGuidedActionsStylist extends GuidedActionsStylist { private static boolean sInitialized; private static float sWidthWeight; @@ -68,11 +65,13 @@ public class DvrGuidedActionsStylist extends GuidedActionsStylist { return; } sInitialized = true; - sItemHeight = context.getResources().getDimensionPixelSize( - R.dimen.dvr_settings_one_line_action_container_height); + sItemHeight = + context.getResources() + .getDimensionPixelSize( + R.dimen.dvr_settings_one_line_action_container_height); TypedValue outValue = new TypedValue(); - context.getResources().getValue(R.dimen.dvr_settings_button_actions_list_width_weight, - outValue, true); + context.getResources() + .getValue(R.dimen.dvr_settings_button_actions_list_width_weight, outValue, true); sWidthWeight = outValue.getFloat(); } } diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index ab852e10..a900cc70 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -26,31 +26,23 @@ import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; +import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrStorageStatusManager; - import java.util.List; public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { - /** - * Action ID for "recording/scheduling the program anyway". - */ + /** Action ID for "recording/scheduling the program anyway". */ public static final int ACTION_RECORD_ANYWAY = 1; - /** - * Action ID for "deleting existed recordings". - */ + /** Action ID for "deleting existed recordings". */ public static final int ACTION_DELETE_RECORDINGS = 2; - /** - * Action ID for "cancelling current recording request". - */ + /** Action ID for "cancelling current recording request". */ public static final int ACTION_CANCEL_RECORDING = 3; + public static final String UNKNOWN_DVR_ACTION = "Unknown DVR Action"; private DvrManager mDvrManager; @@ -63,13 +55,13 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - ApplicationSingletons singletons = TvApplication.getSingletons(context); + TvSingletons singletons = TvSingletons.getSingletons(context); mDvrManager = singletons.getDvrManager(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); VerticalGridView actionsList = getGuidedActionsStylist().getActionsGridView(); actionsList.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE); @@ -122,32 +114,37 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { } /** - * The inner guided step fragment for - * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * The inner guided step fragment for {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment * .DvrNoFreeSpaceErrorDialogFragment}. */ public static class DvrNoFreeSpaceErrorFragment extends DvrGuidedStepFragment { @Override public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title), - getString(R.string.dvr_error_no_free_space_description), null, null); + return new GuidanceStylist.Guidance( + getString(R.string.dvr_error_no_free_space_title), + getString(R.string.dvr_error_no_free_space_description), + null, + null); } @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_RECORD_ANYWAY) - .title(R.string.dvr_action_record_anyway) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_DELETE_RECORDINGS) - .title(R.string.dvr_action_delete_recordings) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL_RECORDING) - .title(R.string.dvr_action_record_cancel) - .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_DELETE_RECORDINGS) + .title(R.string.dvr_action_delete_recordings) + .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_CANCEL_RECORDING) + .title(R.string.dvr_action_record_cancel) + .build()); } @Override @@ -157,29 +154,32 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { } /** - * The inner guided step fragment for - * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * The inner guided step fragment for {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment * .DvrSmallSizedStorageErrorDialogFragment}. */ public static class DvrSmallSizedStorageErrorFragment extends DvrGuidedStepFragment { @Override public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString( - R.string.dvr_error_small_sized_storage_title); - String description = getResources().getString( - R.string.dvr_error_small_sized_storage_description, - DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024 - / 1024 / 1024); + String title = getResources().getString(R.string.dvr_error_small_sized_storage_title); + String description = + getResources() + .getString( + R.string.dvr_error_small_sized_storage_description, + RecordingStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES + / 1024 + / 1024 + / 1024); return new GuidanceStylist.Guidance(title, description, null, null); } @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(GuidedAction.ACTION_ID_OK) - .title(android.R.string.ok) - .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(GuidedAction.ACTION_ID_OK) + .title(android.R.string.ok) + .build()); } @Override @@ -192,4 +192,4 @@ public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { return "DvrSmallSizedStorageErrorFragment"; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index f8ef3850..4a713703 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -20,47 +20,26 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; -import java.util.List; - public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { - /** - * Key for input ID. - * Type: String. - */ + /** Key for input ID. Type: String. */ public static final String KEY_INPUT_ID = "DvrHalfSizedDialogFragment.input_id"; - /** - * Key for the program. - * Type: {@link com.android.tv.data.Program}. - */ + /** Key for the program. Type: {@link com.android.tv.data.Program}. */ public static final String KEY_PROGRAM = "DvrHalfSizedDialogFragment.program"; - /** - * Key for the channel ID. - * Type: long. - */ + /** Key for the channel ID. Type: long. */ public static final String KEY_CHANNEL_ID = "DvrHalfSizedDialogFragment.channel_id"; - /** - * Key for the recording start time in millisecond. - * Type: long. - */ + /** Key for the recording start time in millisecond. Type: long. */ public static final String KEY_START_TIME_MS = "DvrHalfSizedDialogFragment.start_time_ms"; - /** - * Key for the recording end time in millisecond. - * Type: long. - */ + /** Key for the recording end time in millisecond. Type: long. */ public static final String KEY_END_TIME_MS = "DvrHalfSizedDialogFragment.end_time_ms"; @Override @@ -93,14 +72,14 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { private DvrGuidedStepFragment mFragment; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); mFragment = onCreateGuidedStepFragment(); mFragment.setArguments(getArguments()); mFragment.setOnActionClickListener(getOnActionClickListener()); - GuidedStepFragment.add(getChildFragmentManager(), - mFragment, R.id.halfsized_dialog_host); + GuidedStepFragment.add( + getChildFragmentManager(), mFragment, R.id.halfsized_dialog_host); return view; } @@ -158,19 +137,15 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } /** A dialog fragment for {@link DvrMissingStorageErrorFragment}. */ - public static class DvrMissingStorageErrorDialogFragment - extends DvrGuidedStepDialogFragment { + public static class DvrMissingStorageErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrMissingStorageErrorFragment(); } } - /** - * A dialog fragment to show error message when there is no enough free space to record. - */ - public static class DvrNoFreeSpaceErrorDialogFragment - extends DvrGuidedStepDialogFragment { + /** A dialog fragment to show error message when there is no enough free space to record. */ + public static class DvrNoFreeSpaceErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment(); @@ -178,8 +153,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } /** - * A dialog fragment to show error message when the current storage is too small to - * support DVR + * A dialog fragment to show error message when the current storage is too small to support DVR */ public static class DvrSmallSizedStorageErrorDialogFragment extends DvrGuidedStepDialogFragment { @@ -212,4 +186,12 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { return new DvrAlreadyRecordedFragment(); } } -}
\ No newline at end of file + + /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */ + public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrFutureProgramInfoFragment(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java index 182416b6..6fba4d98 100644 --- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java @@ -22,19 +22,15 @@ import android.content.Intent; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; - import java.util.ArrayList; import java.util.List; public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { - /** - * Key for the failed scheduled recordings information. - */ + /** Key for the failed scheduled recordings information. */ public static final String FAILED_SCHEDULED_RECORDING_INFOS = "failed_scheduled_recording_infos"; @@ -54,7 +50,8 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { } SoftPreconditions.checkState( mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(), - TAG, "failed scheduled recording is null"); + TAG, + "failed scheduled recording is null"); } @Override @@ -63,28 +60,39 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { String description; int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size(); if (failedScheduledRecordingSize == 1) { - title = getString( - R.string.dvr_error_insufficient_space_title_one_recording, - mFailedScheduledRecordingInfos.get(0)); - description = getString( - R.string.dvr_error_insufficient_space_description_one_recording, - mFailedScheduledRecordingInfos.get(0)); + title = + getString( + R.string.dvr_error_insufficient_space_title_one_recording, + mFailedScheduledRecordingInfos.get(0)); + description = + getString( + R.string.dvr_error_insufficient_space_description_one_recording, + mFailedScheduledRecordingInfos.get(0)); } else if (failedScheduledRecordingSize == 2) { - title = getString( - R.string.dvr_error_insufficient_space_title_two_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); - description = getString( - R.string.dvr_error_insufficient_space_description_two_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + title = + getString( + R.string.dvr_error_insufficient_space_title_two_recordings, + mFailedScheduledRecordingInfos.get(0), + mFailedScheduledRecordingInfos.get(1)); + description = + getString( + R.string.dvr_error_insufficient_space_description_two_recordings, + mFailedScheduledRecordingInfos.get(0), + mFailedScheduledRecordingInfos.get(1)); } else { - title = getString( - R.string.dvr_error_insufficient_space_title_three_or_more_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), - mFailedScheduledRecordingInfos.get(2)); - description = getString( - R.string.dvr_error_insufficient_space_description_three_or_more_recordings, - mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), - mFailedScheduledRecordingInfos.get(2)); + title = + getString( + R.string.dvr_error_insufficient_space_title_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), + mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + description = + getString( + R.string + .dvr_error_insufficient_space_description_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), + mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); } return new Guidance(title, description, null, null); } @@ -92,15 +100,18 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); - if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) { - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_VIEW_RECENT_RECORDINGS) - .title(getResources().getString( - R.string.dvr_error_insufficient_space_action_view_recent_recordings)) - .build()); + actions.add( + new GuidedAction.Builder(activity).clickAction(GuidedAction.ACTION_ID_OK).build()); + if (TvSingletons.getSingletons(getContext()).getDvrManager().hasValidItems()) { + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_VIEW_RECENT_RECORDINGS) + .title( + getResources() + .getString( + R.string + .dvr_error_insufficient_space_action_view_recent_recordings)) + .build()); } } diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index e726995f..e5f40260 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -24,10 +24,8 @@ import android.provider.Settings; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.util.Log; - import com.android.tv.R; import com.android.tv.dvr.ui.browse.DvrDetailsActivity; - import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @@ -44,22 +42,24 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getResources().getString(R.string.dvr_error_missing_storage_title); - String description = getResources().getString( - R.string.dvr_error_missing_storage_description); + String description = + getResources().getString(R.string.dvr_error_missing_storage_description); return new Guidance(title, description, null, null); } @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OK) - .title(android.R.string.ok) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OPEN_STORAGE_SETTINGS) - .title(getResources().getString(R.string.dvr_action_error_storage_settings)) - .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_OK) + .title(android.R.string.ok) + .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_OPEN_STORAGE_SETTINGS) + .title(getResources().getString(R.string.dvr_action_error_storage_settings)) + .build()); } @Override diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java index e4cb7243..5bb97e90 100644 --- a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java @@ -16,9 +16,11 @@ package com.android.tv.dvr.ui; +import android.annotation.TargetApi; import android.app.FragmentManager; import android.content.Context; import android.graphics.Typeface; +import android.os.Build; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; @@ -26,23 +28,20 @@ import android.support.v17.leanback.widget.GuidedActionsStylist; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.data.SeriesRecording; - import java.util.ArrayList; import java.util.List; /** Fragment for DVR series recording settings. */ +@TargetApi(Build.VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { - /** - * Name of series recording id starting the fragment. - * Type: Long - */ + /** Name of series recording id starting the fragment. Type: Long */ public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id"; private static final int ONE_TIME_RECORDING_ID = 0; @@ -61,14 +60,14 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { public void onAttach(Context context) { super.onAttach(context); mSeriesRecordings.clear(); - mSeriesRecordings.add(new SeriesRecording.Builder() - .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) - .setPriority(Long.MAX_VALUE) - .setId(ONE_TIME_RECORDING_ID) - .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - long comeFromSeriesRecordingId = - getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); + mSeriesRecordings.add( + new SeriesRecording.Builder() + .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) + .setPriority(Long.MAX_VALUE) + .setId(ONE_TIME_RECORDING_ID) + .build()); + DvrDataManager dvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); + long comeFromSeriesRecordingId = getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL || series.getId() == comeFromSeriesRecordingId) { @@ -86,52 +85,62 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { @Override public void onResume() { super.onResume(); - setSelectedActionPosition(mComeFromSeriesRecording == null ? 1 - : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); + setSelectedActionPosition( + mComeFromSeriesRecording == null + ? 1 + : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); } @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = mComeFromSeriesRecording == null ? null - : mComeFromSeriesRecording.getTitle(); - return new Guidance(getString(R.string.dvr_priority_title), - getString(R.string.dvr_priority_description), breadcrumb, null); + String breadcrumb = + mComeFromSeriesRecording == null ? null : mComeFromSeriesRecording.getTitle(); + return new Guidance( + getString(R.string.dvr_priority_title), + getString(R.string.dvr_priority_description), + breadcrumb, + null); } @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { int position = 0; for (SeriesRecording seriesRecording : mSeriesRecordings) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(position++) - .title(seriesRecording.getTitle()) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(position++) + .title(seriesRecording.getTitle()) + .build()); } } @Override public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SAVE) - .title(getString(R.string.dvr_priority_button_action_save)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SAVE) + .title(getString(R.string.dvr_priority_button_action_save)) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); } @Override public void onTrackedGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_SAVE) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); int size = mSeriesRecordings.size(); for (int i = 1; i < size; ++i) { long priority = DvrScheduleManager.suggestSeriesPriority(size - i); SeriesRecording seriesRecording = mSeriesRecordings.get(i); if (seriesRecording.getPriority() != priority) { - dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording) - .setPriority(priority).build()); + dvrManager.updateSeriesRecording( + SeriesRecording.buildFrom(seriesRecording) + .setPriority(priority) + .build()); } } FragmentManager fragmentManager = getFragmentManager(); @@ -222,8 +231,9 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { private void updateItem(View itemView, int position) { GuidedAction action = getActions().get(position); action.setTitle(mSeriesRecordings.get(position).getTitle()); - boolean selected = mSelectedRecording != null - && mSeriesRecordings.indexOf(mSelectedRecording) == position; + boolean selected = + mSelectedRecording != null + && mSeriesRecordings.indexOf(mSelectedRecording) == position; TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title); ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image); if (position == 0) { @@ -259,4 +269,4 @@ public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index 390e0928..5251e140 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -26,9 +26,8 @@ import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; @@ -36,21 +35,17 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; - import java.util.Collections; import java.util.List; /** * A fragment which asks the user the type of the recording. - * <p> - * The program should be episodic and the series recording should not had been created yet. + * + * <p>The program should be episodic and the series recording should not had been created yet. */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { - /** - * Key for the whether to add the current program to series. - * Type: boolean - */ + /** Key for the whether to add the current program to series. Type: boolean */ public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series"; private static final String TAG = "DvrScheduleFragment"; @@ -68,13 +63,18 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false); } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, - "The program should be episodic: " + mProgram); + DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); + SoftPreconditions.checkArgument( + mProgram != null && mProgram.isEpisodic(), + TAG, + "The program should be episodic: %s ", + mProgram); SeriesRecording seriesRecording = dvrManager.getSeriesRecording(mProgram); - SoftPreconditions.checkArgument(seriesRecording == null - || seriesRecording.isStopped(), TAG, - "The series recording should be stopped or null: " + seriesRecording); + SoftPreconditions.checkArgument( + seriesRecording == null || seriesRecording.isStopped(), + TAG, + "The series recording should be stopped or null: %s", + seriesRecording); super.onCreate(savedInstanceState); } @@ -96,23 +96,33 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { Context context = getContext(); String description; if (mProgram.getStartTimeUtcMillis() <= System.currentTimeMillis()) { - description = getString(R.string.dvr_action_record_episode_from_now_description, - DateUtils.formatDateTime(context, mProgram.getEndTimeUtcMillis(), - DateUtils.FORMAT_SHOW_TIME)); + description = + getString( + R.string.dvr_action_record_episode_from_now_description, + DateUtils.formatDateTime( + context, + mProgram.getEndTimeUtcMillis(), + DateUtils.FORMAT_SHOW_TIME)); } else { - description = Utils.getDurationString(context, mProgram.getStartTimeUtcMillis(), - mProgram.getEndTimeUtcMillis(), true); + description = + Utils.getDurationString( + context, + mProgram.getStartTimeUtcMillis(), + mProgram.getEndTimeUtcMillis(), + true); } - actions.add(new GuidedAction.Builder(context) - .id(ACTION_RECORD_EPISODE) - .title(R.string.dvr_action_record_episode) - .description(description) - .build()); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_RECORD_SERIES) - .title(R.string.dvr_action_record_series) - .description(mProgram.getTitle()) - .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_RECORD_EPISODE) + .title(R.string.dvr_action_record_episode) + .description(description) + .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_RECORD_SERIES) + .title(R.string.dvr_action_record_series) + .description(mProgram.getTitle()) + .build()); } @Override @@ -121,34 +131,50 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { getDvrManager().addSchedule(mProgram); List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules(mProgram); if (conflicts.isEmpty()) { - DvrUiHelper.showAddScheduleToast(getContext(), mProgram.getTitle(), - mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis()); + DvrUiHelper.showAddScheduleToast( + getContext(), + mProgram.getTitle(), + mProgram.getStartTimeUtcMillis(), + mProgram.getEndTimeUtcMillis()); dismissDialog(); } else { GuidedStepFragment fragment = new DvrProgramConflictFragment(); Bundle args = new Bundle(); args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, mProgram); fragment.setArguments(args); - GuidedStepFragment.add(getFragmentManager(), fragment, - R.id.halfsized_dialog_host); + GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); } } else if (action.getId() == ACTION_RECORD_SERIES) { - SeriesRecording seriesRecording = TvApplication.getSingletons(getContext()) - .getDvrDataManager().getSeriesRecording(mProgram.getSeriesId()); + SeriesRecording seriesRecording = + TvSingletons.getSingletons(getContext()) + .getDvrDataManager() + .getSeriesRecording(mProgram.getSeriesId()); if (seriesRecording == null) { - seriesRecording = getDvrManager().addSeriesRecording(mProgram, - Collections.emptyList(), SeriesRecording.STATE_SERIES_STOPPED); + seriesRecording = + getDvrManager() + .addSeriesRecording( + mProgram, + Collections.emptyList(), + SeriesRecording.STATE_SERIES_STOPPED); } else { // Reset priority to the highest. - seriesRecording = SeriesRecording.buildFrom(seriesRecording) - .setPriority(TvApplication.getSingletons(getContext()) - .getDvrScheduleManager().suggestNewSeriesPriority()) - .build(); + seriesRecording = + SeriesRecording.buildFrom(seriesRecording) + .setPriority( + TvSingletons.getSingletons(getContext()) + .getDvrScheduleManager() + .suggestNewSeriesPriority()) + .build(); getDvrManager().updateSeriesRecording(seriesRecording); } - DvrUiHelper.startSeriesSettingsActivity(getContext(), - seriesRecording.getId(), null, true, true, true, + DvrUiHelper.startSeriesSettingsActivity( + getContext(), + seriesRecording.getId(), + null, + true, + true, + true, mAddCurrentProgramToSeries ? mProgram : null); dismissDialog(); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index 667af34a..a2ae1f97 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -19,22 +19,17 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; -/** - * Activity to show details view in DVR. - */ +/** Activity to show details view in DVR. */ public class DvrSeriesDeletionActivity extends Activity { - /** - * Name of series id added to the Intent. - */ + /** Name of series id added to the Intent. */ public static final String SERIES_RECORDING_ID = "series_recording_id"; @Override public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java index 8bf8560f..685f0a58 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -26,9 +26,8 @@ import android.support.v17.leanback.widget.GuidedActionsStylist; import android.text.TextUtils; import android.view.ViewGroup.LayoutParams; import android.widget.Toast; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; @@ -37,7 +36,6 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -45,9 +43,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -/** - * Fragment for DVR series recording settings. - */ +/** Fragment for DVR series recording settings. */ public class DvrSeriesDeletionFragment extends GuidedStepFragment { private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); @@ -68,18 +64,23 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mSeriesRecordingId = getArguments() - .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); + mSeriesRecordingId = + getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); SoftPreconditions.checkArgument(mSeriesRecordingId != -1); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); + TvSingletons.getSingletons(context).getDvrWatchedPositionManager(); mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); - mOneLineActionHeight = getResources().getDimensionPixelSize( - R.dimen.dvr_settings_one_line_action_container_height); + mOneLineActionHeight = + getResources() + .getDimensionPixelSize( + R.dimen.dvr_settings_one_line_action_container_height); if (mRecordings.isEmpty()) { - Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings), - Toast.LENGTH_LONG).show(); + Toast.makeText( + getActivity(), + getString(R.string.dvr_series_deletion_no_recordings), + Toast.LENGTH_LONG) + .show(); finishGuidedStepFragments(); return; } @@ -93,28 +94,35 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { if (series != null) { breadcrumb = series.getTitle(); } - return new Guidance(getString(R.string.dvr_series_deletion_title), - getString(R.string.dvr_series_deletion_description), breadcrumb, null); + return new Guidance( + getString(R.string.dvr_series_deletion_title), + getString(R.string.dvr_series_deletion_description), + breadcrumb, + null); } @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_WATCHED) - .title(getString(R.string.dvr_series_select_watched)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_ALL) - .title(getString(R.string.dvr_series_select_all)) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_WATCHED) + .title(getString(R.string.dvr_series_select_watched)) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_ALL) + .title(getString(R.string.dvr_series_select_all)) + .build()); actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); for (RecordedProgram recording : mRecordings) { long watchedPositionMs = mDvrWatchedPositionManager.getWatchedPosition(recording.getId()); String title = recording.getEpisodeDisplayTitle(getContext()); if (TextUtils.isEmpty(title)) { - title = TextUtils.isEmpty(recording.getTitle()) ? - getString(R.string.channel_banner_no_title) : recording.getTitle(); + title = + TextUtils.isEmpty(recording.getTitle()) + ? getString(R.string.channel_banner_no_title) + : recording.getTitle(); } String description; if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { @@ -123,24 +131,27 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { } else { description = getString(R.string.dvr_series_never_watched); } - actions.add(new GuidedAction.Builder(getActivity()) - .id(recording.getId()) - .title(title) - .description(description) - .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(recording.getId()) + .title(title) + .description(description) + .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) + .build()); } } @Override public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_DELETE) - .title(getString(R.string.dvr_detail_delete)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_DELETE) + .title(getString(R.string.dvr_detail_delete)) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); } @Override @@ -155,12 +166,19 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { } } if (!idsToDelete.isEmpty()) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager(); dvrManager.removeRecordedPrograms(idsToDelete); } - Toast.makeText(getContext(), getResources().getQuantityString( - R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), - mRecordings.size()), Toast.LENGTH_LONG).show(); + Toast.makeText( + getContext(), + getResources() + .getQuantityString( + R.plurals.dvr_msg_episodes_deleted, + idsToDelete.size(), + idsToDelete.size(), + mRecordings.size()), + Toast.LENGTH_LONG) + .show(); finishGuidedStepFragments(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { finishGuidedStepFragments(); @@ -218,13 +236,17 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { - return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), - Utils.getRoundOffMinsFromMs(durationMs)); + return getResources() + .getString( + R.string.dvr_series_watched_info_minutes, + Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), + Utils.getRoundOffMinsFromMs(durationMs)); } else { - return getResources().getString(R.string.dvr_series_watched_info_seconds, - Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), - TimeUnit.MILLISECONDS.toSeconds(durationMs)); + return getResources() + .getString( + R.string.dvr_series_watched_info_seconds, + Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), + TimeUnit.MILLISECONDS.toSeconds(durationMs)); } } @@ -246,8 +268,10 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { } private void updateSelectAllState(GuidedAction selectAll, boolean select) { - selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all) - : getString(R.string.dvr_series_select_all)); + selectAll.setTitle( + select + ? getString(R.string.dvr_series_deselect_all) + : getString(R.string.dvr_series_select_all)); notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL)); } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java index 1a0d13d3..9acb5b5e 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java @@ -19,18 +19,13 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; - import com.android.tv.R; public class DvrSeriesScheduledDialogActivity extends Activity { - /** - * Name of series recording id added to the Intent. - */ + /** Name of series recording id added to the Intent. */ public static final String SERIES_RECORDING_ID = "series_recording_id"; - /** - * Name of flag to check if the dialog should show view schedule option. - */ + /** Name of flag to check if the dialog should show view schedule option. */ public static final String SHOW_VIEW_SCHEDULE_OPTION = "show_view_schedule_option"; @Override @@ -41,8 +36,8 @@ public class DvrSeriesScheduledDialogActivity extends Activity { DvrSeriesScheduledFragment dvrSeriesScheduledFragment = new DvrSeriesScheduledFragment(); dvrSeriesScheduledFragment.setArguments(getIntent().getExtras()); - GuidedStepFragment.addAsRoot(this, dvrSeriesScheduledFragment, - R.id.halfsized_dialog_host); + GuidedStepFragment.addAsRoot( + this, dvrSeriesScheduledFragment, R.id.halfsized_dialog_host); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index 2c4bb3ea..edb62c96 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -22,28 +22,26 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.Program; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; - import java.util.List; public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { /** - * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. - * Type: List<{@link Program}> + * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. Type: + * List<{@link Program}> */ public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs"; - private final static long SERIES_RECORDING_ID_NOT_SET = -1; + private static final long SERIES_RECORDING_ID_NOT_SET = -1; - private final static int ACTION_VIEW_SCHEDULES = 1; + private static final int ACTION_VIEW_SCHEDULES = 1; private SeriesRecording mSeriesRecording; private boolean mShowViewScheduleOption; @@ -57,26 +55,35 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - long seriesRecordingId = getArguments().getLong( - DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, SERIES_RECORDING_ID_NOT_SET); + long seriesRecordingId = + getArguments() + .getLong( + DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, + SERIES_RECORDING_ID_NOT_SET); if (seriesRecordingId == SERIES_RECORDING_ID_NOT_SET) { getActivity().finish(); return; } - mShowViewScheduleOption = getArguments().getBoolean( - DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); - mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() - .getSeriesRecording(seriesRecordingId); + mShowViewScheduleOption = + getArguments() + .getBoolean(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); + mSeriesRecording = + TvSingletons.getSingletons(context) + .getDvrDataManager() + .getSeriesRecording(seriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); BigArguments.reset(); - mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() - .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + mSchedulesAddedCount = + TvSingletons.getSingletons(getContext()) + .getDvrManager() + .getAvailableScheduledRecording(mSeriesRecording.getId()) + .size(); DvrScheduleManager dvrScheduleManager = - TvApplication.getSingletons(context).getDvrScheduleManager(); + TvSingletons.getSingletons(context).getDvrScheduleManager(); List<ScheduledRecording> conflictingRecordings = dvrScheduleManager.getConflictingSchedules(mSeriesRecording); mHasConflict = !conflictingRecordings.isEmpty(); @@ -87,7 +94,7 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { ++mOutThisSeriesConflictCount; } } - } + } @Override public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { @@ -104,14 +111,14 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Context context = getContext(); - actions.add(new GuidedAction.Builder(context) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); + actions.add( + new GuidedAction.Builder(context).clickAction(GuidedAction.ACTION_ID_OK).build()); if (mShowViewScheduleOption) { - actions.add(new GuidedAction.Builder(context) - .id(ACTION_VIEW_SCHEDULES) - .title(R.string.dvr_action_view_schedules) - .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_VIEW_SCHEDULES) + .title(R.string.dvr_action_view_schedules) + .build()); } } @@ -119,13 +126,15 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_VIEW_SCHEDULES) { Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class); - intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity - .TYPE_SERIES_SCHEDULE); - intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, + intent.putExtra( + DvrSchedulesActivity.KEY_SCHEDULES_TYPE, + DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); + intent.putExtra( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, mSeriesRecording); BigArguments.reset(); - BigArguments.setArgument(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); + BigArguments.setArgument( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); startActivity(intent); } getActivity().finish(); @@ -148,33 +157,47 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { private String getDescription() { if (!mHasConflict) { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount, - mSchedulesAddedCount, mSeriesRecording.getTitle()); + return getResources() + .getQuantityString( + R.plurals.dvr_series_scheduled_no_conflict, + mSchedulesAddedCount, + mSchedulesAddedCount, + mSeriesRecording.getTitle()); } else { // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means // mHasConflict is false. So we don't need to check that case. if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_this_and_other_series_conflict, - mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), - mInThisSeriesConflictCount + mOutThisSeriesConflictCount); + return getResources() + .getQuantityString( + R.plurals.dvr_series_scheduled_this_and_other_series_conflict, + mSchedulesAddedCount, + mSchedulesAddedCount, + mSeriesRecording.getTitle(), + mInThisSeriesConflictCount + mOutThisSeriesConflictCount); } else if (mInThisSeriesConflictCount != 0) { - return getResources().getQuantityString( - R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, - mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), - mInThisSeriesConflictCount); + return getResources() + .getQuantityString( + R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, + mSchedulesAddedCount, + mSchedulesAddedCount, + mSeriesRecording.getTitle(), + mInThisSeriesConflictCount); } else { if (mOutThisSeriesConflictCount == 1) { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_only_other_series_one_conflict, - mSchedulesAddedCount, mSchedulesAddedCount, - mSeriesRecording.getTitle()); + return getResources() + .getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_one_conflict, + mSchedulesAddedCount, + mSchedulesAddedCount, + mSeriesRecording.getTitle()); } else { - return getResources().getQuantityString( - R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, - mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), - mOutThisSeriesConflictCount); + return getResources() + .getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, + mSchedulesAddedCount, + mSchedulesAddedCount, + mSeriesRecording.getTitle(), + mOutThisSeriesConflictCount); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 6dd20b3a..1a51cf46 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -20,34 +20,27 @@ import android.app.Activity; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; import com.android.tv.common.SoftPreconditions; -/** - * Activity to show details view in DVR. - */ +/** Activity to show details view in DVR. */ public class DvrSeriesSettingsActivity extends Activity { - /** - * Name of series id added to the Intent. - * Type: Long - */ + /** Name of series id added to the Intent. Type: Long */ public static final String SERIES_RECORDING_ID = "series_recording_id"; /** * Name of the boolean flag to decide if the series recording with empty schedule and recording - * will be removed. - * Type: boolean + * will be removed. Type: boolean */ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; /** - * Name of the boolean flag to decide if the setting fragment should be translucent. - * Type: boolean + * Name of the boolean flag to decide if the setting fragment should be translucent. Type: + * boolean */ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent"; /** - * Name of the program list. The list contains the programs which belong to the series. - * Type: List<{@link com.android.tv.data.Program}> + * Name of the program list. The list contains the programs which belong to the series. Type: + * List<{@link com.android.tv.data.Program}> */ public static final String PROGRAM_LIST = "program_list"; @@ -67,7 +60,7 @@ public class DvrSeriesSettingsActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); long seriesRecordingId = getIntent().getLongExtra(SERIES_RECORDING_ID, -1); @@ -83,8 +76,9 @@ public class DvrSeriesSettingsActivity extends Activity { @Override public void onAttachedToWindow() { if (!getIntent().getExtras().getBoolean(IS_WINDOW_TRANSLUCENT, true)) { - getWindow().setBackgroundDrawable( - new ColorDrawable(getColor(R.color.common_tv_background))); + getWindow() + .setBackgroundDrawable( + new ColorDrawable(getColor(R.color.common_tv_background))); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java index f28382da..eadb3b9e 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java @@ -16,20 +16,22 @@ package com.android.tv.dvr.ui; +import android.annotation.TargetApi; import android.app.FragmentManager; import android.content.Context; +import android.os.Build; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.util.LongSparseArray; - import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelImpl; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; @@ -37,20 +39,18 @@ import com.android.tv.dvr.data.SeasonEpisodeNumber; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.data.SeriesRecording.ChannelOption; import com.android.tv.dvr.recorder.SeriesRecordingScheduler; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -/** - * Fragment for DVR series recording settings. - */ +/** Fragment for DVR series recording settings. */ +@TargetApi(Build.VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public class DvrSeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { private static final String TAG = "SeriesSettingsFragment"; - private static final boolean DEBUG = false; private static final long ACTION_ID_PRIORITY = 10; private static final long ACTION_ID_CHANNEL = 11; @@ -85,19 +85,20 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment public void onAttach(Context context) { super.onAttach(context); mBackStackCount = getFragmentManager().getBackStackEntryCount(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } - mShowViewScheduleOptionInDialog = getArguments().getBoolean( - DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); + mShowViewScheduleOptionInDialog = + getArguments() + .getBoolean(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); mDvrDataManager.addSeriesRecordingListener(this); - mPrograms = (List<Program>) BigArguments.getArgument( - DvrSeriesSettingsActivity.PROGRAM_LIST); + mPrograms = + (List<Program>) BigArguments.getArgument(DvrSeriesSettingsActivity.PROGRAM_LIST); BigArguments.reset(); if (mPrograms == null) { getActivity().finish(); @@ -105,7 +106,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } Set<Long> channelIds = new HashSet<>(); ChannelDataManager channelDataManager = - TvApplication.getSingletons(context).getChannelDataManager(); + TvSingletons.getSingletons(context).getChannelDataManager(); for (Program program : mPrograms) { long channelId = program.getChannelId(); if (channelIds.add(channelId)) { @@ -126,7 +127,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; } } - mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR); + mChannels.sort(ChannelImpl.CHANNEL_NUMBER_COMPARATOR); mFragmentTitle = getString(R.string.dvr_series_settings_title); mProrityActionTitle = getString(R.string.dvr_series_settings_priority); mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); @@ -150,8 +151,9 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment @Override public void onDestroy() { - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount + && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); } super.onDestroy(); @@ -166,29 +168,33 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment @Override public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - mPriorityGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_PRIORITY) - .title(mProrityActionTitle) - .build(); + mPriorityGuidedAction = + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_PRIORITY) + .title(mProrityActionTitle) + .build(); actions.add(mPriorityGuidedAction); - mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_CHANNEL) - .title(mChannelsActionTitle) - .subActions(buildChannelSubAction()) - .build(); + mChannelsGuidedAction = + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_CHANNEL) + .title(mChannelsActionTitle) + .subActions(buildChannelSubAction()) + .build(); actions.add(mChannelsGuidedAction); updateChannelsGuidedAction(false); } @Override public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); } @Override @@ -199,16 +205,18 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment || mSeriesRecording.isStopped() || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE && mSeriesRecording.getChannelId() != mSelectedChannelId)) { - SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) - .setChannelOption(mChannelOption) - .setState(SeriesRecording.STATE_SERIES_NORMAL); + SeriesRecording.Builder builder = + SeriesRecording.buildFrom(mSeriesRecording) + .setChannelOption(mChannelOption) + .setState(SeriesRecording.STATE_SERIES_NORMAL); if (mSelectedChannelId != Channel.INVALID_ID) { builder.setChannelId(mSelectedChannelId); } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - dvrManager.updateSeriesRecording(builder.build()); - if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL - || mSelectedChannelId == mCurrentProgram.getChannelId())) { + DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); + dvrManager.updateSeriesRecording(builder.build()); + if (mCurrentProgram != null + && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL + || mSelectedChannelId == mCurrentProgram.getChannelId())) { dvrManager.addSchedule(mCurrentProgram); } updateSchedulesToSeries(); @@ -222,7 +230,8 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment FragmentManager fragmentManager = getFragmentManager(); DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); Bundle args = new Bundle(); - args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + args.putLong( + DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, mSeriesRecording.getId()); fragment.setArguments(args); GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); @@ -254,9 +263,9 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment private void updateChannelsGuidedAction(boolean notifyActionChanged) { if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else if (mId2Channel.get(mSelectedChannelId) != null){ - mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) - .getDisplayText()); + } else if (mId2Channel.get(mSelectedChannelId) != null) { + mChannelsGuidedAction.setDescription( + mId2Channel.get(mSelectedChannelId).getDisplayText()); } if (notifyActionChanged) { notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); @@ -282,8 +291,8 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } else if (priorityOrder >= totalSeriesCount - 1) { mPriorityGuidedAction.setDescription(mProrityActionLowestText); } else { - mPriorityGuidedAction.setDescription(getString( - R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); + mPriorityGuidedAction.setDescription( + getString(R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); } @@ -294,54 +303,66 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - scheduledEpisodes.add(new SeasonEpisodeNumber( - r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber())); + scheduledEpisodes.add( + new SeasonEpisodeNumber( + r.getSeriesRecordingId(), + r.getSeasonNumber(), + r.getEpisodeNumber())); } } for (Program program : mPrograms) { // Removes current programs and scheduled episodes out, matches the channel option. if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() && mSeriesRecording.matchProgram(program) - && !scheduledEpisodes.contains(new SeasonEpisodeNumber( - mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) { + && !scheduledEpisodes.contains( + new SeasonEpisodeNumber( + mSeriesRecordingId, + program.getSeasonNumber(), + program.getEpisodeNumber()))) { recordingCandidates.add(program); } } if (recordingCandidates.isEmpty()) { return; } - List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( - mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) - .get(mSeriesRecordingId); + List<Program> programsToSchedule = + SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDvrDataManager, + Collections.singletonList(mSeriesRecording), + recordingCandidates) + .get(mSeriesRecordingId); if (!programsToSchedule.isEmpty()) { - TvApplication.getSingletons(getContext()).getDvrManager() + TvSingletons.getSingletons(getContext()) + .getDvrManager() .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); } } private List<GuidedAction> buildChannelSubAction() { List<GuidedAction> channelSubActions = new ArrayList<>(); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ALL) - .title(mChannelsActionAllText) - .build()); + channelSubActions.add( + new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ALL) + .title(mChannelsActionAllText) + .build()); for (Channel channel : mChannels) { - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) - .title(channel.getDisplayText()) - .build()); + channelSubActions.add( + new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) + .title(channel.getDisplayText()) + .build()); } return channelSubActions; } private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, - mShowViewScheduleOptionInDialog, mPrograms); + DvrUiHelper.startSeriesScheduledDialogActivity( + getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog, mPrograms); finishGuidedStepFragments(); } @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {} @Override public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { @@ -363,4 +384,4 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index baa45793..e93387ab 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -25,45 +25,36 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.data.ScheduledRecording; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** * A fragment which asks the user to make a recording schedule for the program. - * <p> - * If the program belongs to a series and the series recording is not created yet, we will show the - * option to record all the episodes of the series. + * + * <p>If the program belongs to a series and the series recording is not created yet, we will show + * the option to record all the episodes of the series. */ @TargetApi(Build.VERSION_CODES.N) public class DvrStopRecordingFragment extends DvrGuidedStepFragment { - /** - * The action ID for the stop action. - */ + /** The action ID for the stop action. */ public static final int ACTION_STOP = 1; - /** - * Key for the program. - * Type: {@link com.android.tv.data.Program}. - */ + /** Key for the program. Type: {@link com.android.tv.data.Program}. */ public static final String KEY_REASON = "DvrStopRecordingFragment.type"; @Retention(RetentionPolicy.SOURCE) @IntDef({REASON_USER_STOP, REASON_ON_CONFLICT}) public @interface ReasonType {} - /** - * The dialog is shown because users want to stop some currently recording program. - */ + /** The dialog is shown because users want to stop some currently recording program. */ public static final int REASON_USER_STOP = 1; /** - * The dialog is shown because users want to record some program that is conflict to the - * current recording program. + * The dialog is shown because users want to record some program that is conflict to the current + * recording program. */ public static final int REASON_ON_CONFLICT = 2; @@ -74,7 +65,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } + public void onScheduledRecordingAdded(ScheduledRecording... schedules) {} @Override public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { @@ -91,7 +82,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { for (ScheduledRecording schedule : schedules) { if (schedule.getId() == mSchedule.getId() && schedule.getState() - != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { dismissDialog(); return; } @@ -109,7 +100,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { dismissDialog(); return; } - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); mStopReason = args.getInt(KEY_REASON); } @@ -128,8 +119,10 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_stop_recording_dialog_title); String description; if (mStopReason == REASON_ON_CONFLICT) { - description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, - mSchedule.getProgramDisplayTitle(getContext())); + description = + getString( + R.string.dvr_stop_recording_dialog_description_on_conflict, + mSchedule.getProgramDisplayTitle(getContext())); } else { description = getString(R.string.dvr_stop_recording_dialog_description); } @@ -140,13 +133,15 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { Context context = getContext(); - actions.add(new GuidedAction.Builder(context) - .id(ACTION_STOP) - .title(R.string.dvr_action_stop) - .build()); - actions.add(new GuidedAction.Builder(context) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); + actions.add( + new GuidedAction.Builder(context) + .id(ACTION_STOP) + .title(R.string.dvr_action_stop) + .build()); + actions.add( + new GuidedAction.Builder(context) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); } @Override @@ -163,4 +158,4 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { return super.getTrackerLabelForGuidedAction(action); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java index 5b880bd6..15abf902 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java @@ -22,18 +22,15 @@ import android.support.v17.leanback.app.GuidedStepFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; -/** - * A dialog fragment which contains {@link DvrStopSeriesRecordingFragment}. - */ +/** A dialog fragment which contains {@link DvrStopSeriesRecordingFragment}. */ public class DvrStopSeriesRecordingDialogFragment extends DialogFragment { public static final String DIALOG_TAG = "dialog_tag"; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.halfsized_dialog, container, false); GuidedStepFragment fragment = new DvrStopSeriesRecordingFragment(); fragment.setArguments(getArguments()); diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java index 7b56cfc1..99211fdb 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -25,25 +25,18 @@ import android.support.v17.leanback.widget.GuidedAction; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; - import java.util.ArrayList; import java.util.List; -/** - * A fragment which asks the user to stop series recording. - */ +/** A fragment which asks the user to stop series recording. */ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { - /** - * Key for the series recording to be stopped. - */ + /** Key for the series recording to be stopped. */ public static final String KEY_SERIES_RECORDING = "key_series_recoridng"; private static final int ACTION_STOP_SERIES_RECORDING = 1; @@ -51,7 +44,8 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { private SeriesRecording mSeriesRecording; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mSeriesRecording = getArguments().getParcelable(KEY_SERIES_RECORDING); return super.onCreateView(inflater, container, savedInstanceState); } @@ -68,19 +62,21 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { @Override public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_STOP_SERIES_RECORDING) - .title(R.string.dvr_series_schedules_stop_dialog_action_stop) - .build()); - actions.add(new GuidedAction.Builder(activity) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); + actions.add( + new GuidedAction.Builder(activity) + .id(ACTION_STOP_SERIES_RECORDING) + .title(R.string.dvr_series_schedules_stop_dialog_action_stop) + .build()); + actions.add( + new GuidedAction.Builder(activity) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); } @Override public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_STOP_SERIES_RECORDING) { - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); DvrManager dvrManager = singletons.getDvrManager(); DvrDataManager dataManager = singletons.getDvrDataManager(); List<ScheduledRecording> toDelete = new ArrayList<>(); @@ -96,8 +92,10 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { if (!toDelete.isEmpty()) { dvrManager.forceRemoveScheduledRecording(ScheduledRecording.toArray(toDelete)); } - dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) - .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + dvrManager.updateSeriesRecording( + SeriesRecording.buildFrom(mSeriesRecording) + .setState(SeriesRecording.STATE_SERIES_STOPPED) + .build()); } dismissDialog(); } diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java index 302fd6cd..16afbdef 100644 --- a/src/com/android/tv/dvr/ui/DvrUiHelper.java +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -37,17 +37,18 @@ import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.widget.ImageView; import android.widget.Toast; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; +import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.recording.RecordingStorageStatusManager; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; @@ -56,6 +57,7 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialog import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrFutureProgramInfoDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; @@ -65,21 +67,19 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErro import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.dvr.ui.list.DvrHistoryActivity; import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; -/** - * A helper class for DVR UI. - */ +/** A helper class for DVR UI. */ @MainThread @TargetApi(Build.VERSION_CODES.N) public class DvrUiHelper { @@ -91,45 +91,43 @@ public class DvrUiHelper { * Checks if the storage status is good for recording and shows error messages if needed. * * @param recordingRequestRunnable if the storage status is OK to record or users choose to - * perform the operation anyway, this Runnable will run. + * perform the operation anyway, this Runnable will run. */ - public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId, - Runnable recordingRequestRunnable) { - if (Utils.isBundledInput(inputId)) { - switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager() + public static void checkStorageStatusAndShowErrorMessage( + Activity activity, String inputId, Runnable recordingRequestRunnable) { + if (CommonUtils.isBundledInput(inputId)) { + switch (TvSingletons.getSingletons(activity) + .getRecordingStorageStatusManager() .getDvrStorageStatus()) { - case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: + case RecordingStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: showDvrSmallSizedStorageErrorDialog(activity); return; - case DvrStorageStatusManager.STORAGE_STATUS_MISSING: + case RecordingStorageStatusManager.STORAGE_STATUS_MISSING: showDvrMissingStorageErrorDialog(activity); return; - case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: + case RecordingStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable); return; + default: // fall out } } recordingRequestRunnable.run(); } - /** - * Shows the schedule dialog. - */ - public static void showScheduleDialog(Activity activity, Program program, - boolean addCurrentProgramToSeries) { + /** Shows the schedule dialog. */ + public static void showScheduleDialog( + Activity activity, Program program, boolean addCurrentProgramToSeries) { if (SoftPreconditions.checkNotNull(program) == null) { return; } Bundle args = new Bundle(); args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, - addCurrentProgramToSeries); + args.putBoolean( + DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, addCurrentProgramToSeries); showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); } - /** - * Shows the recording duration options dialog. - */ + /** Shows the recording duration options dialog. */ public static void showChannelRecordDurationOptions(Activity activity, Channel channel) { if (SoftPreconditions.checkNotNull(channel) == null) { return; @@ -139,9 +137,7 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args); } - /** - * Shows the dialog which says that the new schedule conflicts with others. - */ + /** Shows the dialog which says that the new schedule conflicts with others. */ public static void showScheduleConflictDialog(Activity activity, Program program) { if (program == null) { return; @@ -151,9 +147,7 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true); } - /** - * Shows the conflict dialog for the channel watching. - */ + /** Shows the conflict dialog for the channel watching. */ public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) { if (channel == null) { return; @@ -163,63 +157,60 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args); } - /** - * Shows DVR insufficient space error dialog. - */ - public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, - Set<String> failedScheduledRecordingInfoSet) { + /** Shows DVR insufficient space error dialog. */ + public static void showDvrInsufficientSpaceErrorDialog( + MainActivity activity, Set<String> failedScheduledRecordingInfoSet) { Bundle args = new Bundle(); ArrayList<String> failedScheduledRecordingInfoArray = new ArrayList<>(failedScheduledRecordingInfoSet); - args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, + args.putStringArrayList( + DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, failedScheduledRecordingInfoArray); showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args); - Utils.clearRecordingFailedReason(activity, - TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.clearRecordingFailedReason( + activity, TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); Utils.clearFailedScheduledRecordingInfoSet(activity); } /** * Shows DVR no free space error dialog. * - * @param recordingRequestRunnable the recording request to be executed when users choose - * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. + * @param recordingRequestRunnable the recording request to be executed when users choose {@link + * DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. */ - public static void showDvrNoFreeSpaceErrorDialog(Activity activity, - Runnable recordingRequestRunnable) { + public static void showDvrNoFreeSpaceErrorDialog( + Activity activity, Runnable recordingRequestRunnable) { DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment(); - fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() { - @Override - public void onActionClick(long actionId) { - if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { - recordingRequestRunnable.run(); - } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { - Intent intent = new Intent(activity, DvrBrowseActivity.class); - activity.startActivity(intent); - } - } - }); + fragment.setOnActionClickListener( + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { + recordingRequestRunnable.run(); + } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { + Intent intent = new Intent(activity, DvrBrowseActivity.class); + activity.startActivity(intent); + } + } + }); showDialogFragment(activity, fragment, null); } - /** - * Shows DVR missing storage error dialog. - */ + /** Shows DVR missing storage error dialog. */ private static void showDvrMissingStorageErrorDialog(Activity activity) { showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); } - /** - * Shows DVR small sized storage error dialog. - */ + /** Shows DVR small sized storage error dialog. */ public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); } - /** - * Shows stop recording dialog. - */ - public static void showStopRecordingDialog(Activity activity, long channelId, int reason, + /** Shows stop recording dialog. */ + public static void showStopRecordingDialog( + Activity activity, + long channelId, + int reason, HalfSizedDialogFragment.OnActionClickListener listener) { Bundle args = new Bundle(); args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); @@ -229,9 +220,7 @@ public class DvrUiHelper { showDialogFragment(activity, fragment, args); } - /** - * Shows "already scheduled" dialog. - */ + /** Shows "already scheduled" dialog. */ public static void showAlreadyScheduleDialog(Activity activity, Program program) { if (program == null) { return; @@ -241,9 +230,7 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true); } - /** - * Shows "already recorded" dialog. - */ + /** Shows "already recorded" dialog. */ public static void showAlreadyRecordedDialog(Activity activity, Program program) { if (program == null) { return; @@ -253,37 +240,49 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); } + /** Shows program information dialog. */ + public static void showProgramInfoDialog(Activity activity, Program program) { + if (program == null || !BuildConfig.ENG) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true); + } + /** * Handle the request of recording a current program. It will handle creating schedules and * shows the proper dialog and toast message respectively for timed-recording and program * recording cases. * - * @param addProgramToSeries denotes whether the program to be recorded should be added into - * the series recording when users choose to record the entire series. + * @param addProgramToSeries denotes whether the program to be recorded should be added into the + * series recording when users choose to record the entire series. */ - public static void requestRecordingCurrentProgram(Activity activity, - Channel channel, Program program, boolean addProgramToSeries) { + public static void requestRecordingCurrentProgram( + Activity activity, Channel channel, Program program, boolean addProgramToSeries) { if (program == null) { DvrUiHelper.showChannelRecordDurationOptions(activity, channel); } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { - String msg = activity.getString(R.string.dvr_msg_current_program_scheduled, - program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); + String msg = + activity.getString( + R.string.dvr_msg_current_program_scheduled, + program.getTitle(), + Utils.toTimeString(program.getEndTimeUtcMillis(), false)); Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); } } /** - * Handle the request of recording a future program. It will handle creating schedules and - * shows the proper toast message. + * Handle the request of recording a future program. It will handle creating schedules and shows + * the proper toast message. * - * @param addProgramToSeries denotes whether the program to be recorded should be added into - * the series recording when users choose to record the entire series. + * @param addProgramToSeries denotes whether the program to be recorded should be added into the + * series recording when users choose to record the entire series. */ - public static void requestRecordingFutureProgram(Activity activity, - Program program, boolean addProgramToSeries) { + public static void requestRecordingFutureProgram( + Activity activity, Program program, boolean addProgramToSeries) { if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { - String msg = activity.getString( - R.string.dvr_msg_program_scheduled, program.getTitle()); + String msg = activity.getString(R.string.dvr_msg_program_scheduled, program.getTitle()); ToastUtils.show(activity, msg, Toast.LENGTH_SHORT); } } @@ -292,12 +291,12 @@ public class DvrUiHelper { * Handles the action to create the new schedule. It returns {@code true} if the schedule is * added and there's no additional UI, otherwise {@code false}. */ - private static boolean handleCreateSchedule(Activity activity, Program program, - boolean addProgramToSeries) { + private static boolean handleCreateSchedule( + Activity activity, Program program, boolean addProgramToSeries) { if (program == null) { return false; } - DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(activity).getDvrManager(); if (!program.isEpisodic()) { // One time recording. dvrManager.addSchedule(program); @@ -307,18 +306,24 @@ public class DvrUiHelper { } } else { // Show recorded program rather than the schedule. - RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); + RecordedProgram recordedProgram = + dvrManager.getRecordedProgram( + program.getTitle(), + program.getSeasonNumber(), + program.getEpisodeNumber()); if (recordedProgram != null) { DvrUiHelper.showAlreadyRecordedDialog(activity, program); return false; } - ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); + ScheduledRecording duplicate = + dvrManager.getScheduledRecording( + program.getTitle(), + program.getSeasonNumber(), + program.getEpisodeNumber()); if (duplicate != null && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || duplicate.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + || duplicate.getState() + == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { DvrUiHelper.showAlreadyScheduleDialog(activity, program); return false; } @@ -334,39 +339,44 @@ public class DvrUiHelper { return true; } - private static void showDialogFragment(Activity activity, - DvrHalfSizedDialogFragment dialogFragment, Bundle args) { + private static void showDialogFragment( + Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args) { showDialogFragment(activity, dialogFragment, args, false, false); } - private static void showDialogFragment(Activity activity, - DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory, + private static void showDialogFragment( + Activity activity, + DvrHalfSizedDialogFragment dialogFragment, + Bundle args, + boolean keepSidePanelHistory, boolean keepProgramGuide) { dialogFragment.setArguments(args); if (activity instanceof MainActivity) { - ((MainActivity) activity).getOverlayManager() - .showDialogFragment(DvrHalfSizedDialogFragment.DIALOG_TAG, dialogFragment, - keepSidePanelHistory, keepProgramGuide); + ((MainActivity) activity) + .getOverlayManager() + .showDialogFragment( + DvrHalfSizedDialogFragment.DIALOG_TAG, + dialogFragment, + keepSidePanelHistory, + keepProgramGuide); } else { - dialogFragment.show(activity.getFragmentManager(), - DvrHalfSizedDialogFragment.DIALOG_TAG); + dialogFragment.show( + activity.getFragmentManager(), DvrHalfSizedDialogFragment.DIALOG_TAG); } } - /** - * Checks whether channel watch conflict dialog is open or not. - */ + /** Checks whether channel watch conflict dialog is open or not. */ public static boolean isChannelWatchConflictDialogShown(MainActivity activity) { - return activity.getOverlayManager().getCurrentDialog() instanceof - DvrChannelWatchConflictDialogFragment; + return activity.getOverlayManager().getCurrentDialog() + instanceof DvrChannelWatchConflictDialogFragment; } - private static ScheduledRecording getEarliestScheduledRecording(List<ScheduledRecording> - recordings) { + private static ScheduledRecording getEarliestScheduledRecording( + List<ScheduledRecording> recordings) { ScheduledRecording earlistScheduledRecording = null; if (!recordings.isEmpty()) { - Collections.sort(recordings, - ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + Collections.sort( + recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); earlistScheduledRecording = recordings.get(0); } return earlistScheduledRecording; @@ -378,10 +388,10 @@ public class DvrUiHelper { * @param programId the ID of the recorded program going to be played. * @param seekTimeMs the seek position to initial playback. * @param pinChecked {@code true} if the pin code for parental controls has already been - * verified, otherwise {@code false}. + * verified, otherwise {@code false}. */ - public static void startPlaybackActivity(Context context, long programId, - long seekTimeMs, boolean pinChecked) { + public static void startPlaybackActivity( + Context context, long programId, long seekTimeMs, boolean pinChecked) { Intent intent = new Intent(context, DvrPlaybackActivity.class); intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { @@ -391,51 +401,52 @@ public class DvrUiHelper { context.startActivity(intent); } - /** - * Shows the schedules activity to resolve the tune conflict. - */ + /** Shows the schedules activity to resolve the tune conflict. */ public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) { if (channel == null) { return; } - List<ScheduledRecording> conflicts = TvApplication.getSingletons(context).getDvrManager() - .getConflictingSchedulesForTune(channel.getId()); + List<ScheduledRecording> conflicts = + TvSingletons.getSingletons(context) + .getDvrManager() + .getConflictingSchedulesForTune(channel.getId()); startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); } - /** - * Shows the schedules activity to resolve the one time recording conflict. - */ - public static void startSchedulesActivityForOneTimeRecordingConflict(Context context, - List<ScheduledRecording> conflicts) { + /** Shows the schedules activity to resolve the one time recording conflict. */ + public static void startSchedulesActivityForOneTimeRecordingConflict( + Context context, List<ScheduledRecording> conflicts) { startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); } - /** - * Shows the schedules activity with full schedule. - */ - public static void startSchedulesActivity(Context context, ScheduledRecording - focusedScheduledRecording) { + /** Shows the schedules activity with full schedule. */ + public static void startDvrHistoryActivity(Context context) { + Intent intent = new Intent(context, DvrHistoryActivity.class); + context.startActivity(intent); + } + + /** Shows the schedules activity with full schedule. */ + public static void startSchedulesActivity( + Context context, ScheduledRecording focusedScheduledRecording) { Intent intent = new Intent(context, DvrSchedulesActivity.class); - intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, - DvrSchedulesActivity.TYPE_FULL_SCHEDULE); + intent.putExtra( + DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity.TYPE_FULL_SCHEDULE); if (focusedScheduledRecording != null) { - intent.putExtra(DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING, + intent.putExtra( + DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING, focusedScheduledRecording); } context.startActivity(intent); } - /** - * Shows the schedules activity for series recording. - */ - public static void startSchedulesActivityForSeries(Context context, - SeriesRecording seriesRecording) { + /** Shows the schedules activity for series recording. */ + public static void startSchedulesActivityForSeries( + Context context, SeriesRecording seriesRecording) { Intent intent = new Intent(context, DvrSchedulesActivity.class); - intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, - DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); - intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, - seriesRecording); + intent.putExtra( + DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); + intent.putExtra( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, seriesRecording); context.startActivity(intent); } @@ -444,93 +455,125 @@ public class DvrUiHelper { * * @param programs list of programs which belong to the series. */ - public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, - @Nullable List<Program> programs, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + public static void startSeriesSettingsActivity( + Context context, + long seriesRecordingId, + @Nullable List<Program> programs, + boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, + boolean showViewScheduleOptionInDialog, Program currentProgram) { - SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager() - .getSeriesRecording(seriesRecordingId); + SeriesRecording series = + TvSingletons.getSingletons(context) + .getDvrDataManager() + .getSeriesRecording(seriesRecordingId); if (series == null) { return; } if (programs != null) { - startSeriesSettingsActivityInternal(context, seriesRecordingId, programs, - removeEmptySeriesSchedule, isWindowTranslucent, - showViewScheduleOptionInDialog, currentProgram); + startSeriesSettingsActivityInternal( + context, + seriesRecordingId, + programs, + removeEmptySeriesSchedule, + isWindowTranslucent, + showViewScheduleOptionInDialog, + currentProgram); } else { EpisodicProgramLoadTask episodicProgramLoadTask = new EpisodicProgramLoadTask(context, series) { - @Override - protected void onPostExecute(List<Program> loadedPrograms) { - sProgressDialog.dismiss(); - sProgressDialog = null; - startSeriesSettingsActivityInternal(context, seriesRecordingId, - loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms, - removeEmptySeriesSchedule, isWindowTranslucent, - showViewScheduleOptionInDialog, currentProgram); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - sProgressDialog = ProgressDialog.show(context, null, context.getString( - R.string.dvr_series_progress_message_reading_programs), true, true, - new DialogInterface.OnCancelListener() { @Override - public void onCancel(DialogInterface dialogInterface) { - episodicProgramLoadTask.cancel(true); + protected void onPostExecute(List<Program> loadedPrograms) { + sProgressDialog.dismiss(); sProgressDialog = null; + startSeriesSettingsActivityInternal( + context, + seriesRecordingId, + loadedPrograms == null + ? Collections.EMPTY_LIST + : loadedPrograms, + removeEmptySeriesSchedule, + isWindowTranslucent, + showViewScheduleOptionInDialog, + currentProgram); } - }); + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + sProgressDialog = + ProgressDialog.show( + context, + null, + context.getString( + R.string.dvr_series_progress_message_reading_programs), + true, + true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + episodicProgramLoadTask.cancel(true); + sProgressDialog = null; + } + }); episodicProgramLoadTask.execute(); } } - private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId, - @NonNull List<Program> programs, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + private static void startSeriesSettingsActivityInternal( + Context context, + long seriesRecordingId, + @NonNull List<Program> programs, + boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, + boolean showViewScheduleOptionInDialog, Program currentProgram) { - SoftPreconditions.checkState(programs != null, - TAG, "Start series settings activity but programs is null"); + SoftPreconditions.checkState( + programs != null, TAG, "Start series settings activity but programs is null"); Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); BigArguments.reset(); BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs); - intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, - removeEmptySeriesSchedule); + intent.putExtra( + DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, removeEmptySeriesSchedule); intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); - intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, + intent.putExtra( + DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, showViewScheduleOptionInDialog); intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram); context.startActivity(intent); } - /** - * Shows "series recording scheduled" dialog activity. - */ - public static void StartSeriesScheduledDialogActivity(Context context, - SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, + /** Shows "series recording scheduled" dialog activity. */ + public static void startSeriesScheduledDialogActivity( + Context context, + SeriesRecording seriesRecording, + boolean showViewScheduleOptionInDialog, List<Program> programs) { if (seriesRecording == null) { return; } Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); - intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, - seriesRecording.getId()); - intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, + intent.putExtra( + DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, seriesRecording.getId()); + intent.putExtra( + DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, showViewScheduleOptionInDialog); BigArguments.reset(); - BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, - programs); + BigArguments.setArgument( + DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, programs); context.startActivity(intent); } /** - * Shows the details activity for the DVR items. The type of DVR items may be - * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. + * Shows the details activity for the DVR items. The type of DVR items may be {@link + * ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. */ - public static void startDetailsActivity(Activity activity, Object dvrItem, - @Nullable ImageView imageView, boolean hideViewSchedule) { + public static void startDetailsActivity( + Activity activity, + Object dvrItem, + @Nullable ImageView imageView, + boolean hideViewSchedule) { if (dvrItem == null) { return; } @@ -544,6 +587,17 @@ public class DvrUiHelper { viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + && schedule.getRecordedProgramId() != null) { + recordingId = schedule.getRecordedProgramId(); + viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + hideViewSchedule = true; + // TODO(b/72638385): pass detailed error message + intent.putExtra( + DvrDetailsActivity.EXTRA_FAILED_MESSAGE, + activity.getString(R.string.dvr_recording_failed)); } else { return; } @@ -561,89 +615,108 @@ public class DvrUiHelper { intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); Bundle bundle = null; if (imageView != null) { - bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); + bundle = + ActivityOptionsCompat.makeSceneTransitionAnimation( + activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME) + .toBundle(); } activity.startActivity(intent, bundle); } - /** - * Shows the cancel all dialog for series schedules list. - */ - public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, - SeriesRecording seriesRecording) { + /** Shows the cancel all dialog for series schedules list. */ + public static void showCancelAllSeriesRecordingDialog( + DvrSchedulesActivity activity, SeriesRecording seriesRecording) { DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = new DvrStopSeriesRecordingDialogFragment(); Bundle arguments = new Bundle(); - arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, - seriesRecording); + arguments.putParcelable( + DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, seriesRecording); dvrStopSeriesRecordingDialogFragment.setArguments(arguments); - dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), - DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); + dvrStopSeriesRecordingDialogFragment.show( + activity.getFragmentManager(), DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); } - /** - * Shows the series deletion activity. - */ + /** Shows the series deletion activity. */ public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) { Intent intent = new Intent(context, DvrSeriesDeletionActivity.class); intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId); context.startActivity(intent); } - public static void showAddScheduleToast(Context context, - String title, long startTimeMs, long endTimeMs) { - String msg = (startTimeMs > System.currentTimeMillis()) ? - context.getString(R.string.dvr_msg_program_scheduled, title) - : context.getString(R.string.dvr_msg_current_program_scheduled, title, - Utils.toTimeString(endTimeMs, false)); + public static void showAddScheduleToast( + Context context, String title, long startTimeMs, long endTimeMs) { + String msg = + (startTimeMs > System.currentTimeMillis()) + ? context.getString(R.string.dvr_msg_program_scheduled, title) + : context.getString( + R.string.dvr_msg_current_program_scheduled, + title, + Utils.toTimeString(endTimeMs, false)); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } - /** - * Returns the styled schedule's title with its season and episode number. - */ - public static CharSequence getStyledTitleWithEpisodeNumber(Context context, - ScheduledRecording schedule, int episodeNumberStyleResId) { - return getStyledTitleWithEpisodeNumber(context, schedule.getProgramTitle(), - schedule.getSeasonNumber(), schedule.getEpisodeNumber(), episodeNumberStyleResId); + /** Returns the styled schedule's title with its season and episode number. */ + public static CharSequence getStyledTitleWithEpisodeNumber( + Context context, ScheduledRecording schedule, int episodeNumberStyleResId) { + return getStyledTitleWithEpisodeNumber( + context, + schedule.getProgramTitle(), + schedule.getSeasonNumber(), + schedule.getEpisodeNumber(), + episodeNumberStyleResId); } - /** - * Returns the styled program's title with its season and episode number. - */ - public static CharSequence getStyledTitleWithEpisodeNumber(Context context, - BaseProgram program, int episodeNumberStyleResId) { - return getStyledTitleWithEpisodeNumber(context, program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber(), episodeNumberStyleResId); + /** Returns the styled program's title with its season and episode number. */ + public static CharSequence getStyledTitleWithEpisodeNumber( + Context context, BaseProgram program, int episodeNumberStyleResId) { + return getStyledTitleWithEpisodeNumber( + context, + program.getTitle(), + program.getSeasonNumber(), + program.getEpisodeNumber(), + episodeNumberStyleResId); } @NonNull - public static CharSequence getStyledTitleWithEpisodeNumber(Context context, String title, - String seasonNumber, String episodeNumber, int episodeNumberStyleResId) { + public static CharSequence getStyledTitleWithEpisodeNumber( + Context context, + String title, + String seasonNumber, + String episodeNumber, + int episodeNumberStyleResId) { if (TextUtils.isEmpty(title)) { return ""; } SpannableStringBuilder builder; if (TextUtils.isEmpty(seasonNumber) || seasonNumber.equals("0")) { - builder = TextUtils.isEmpty(episodeNumber) ? new SpannableStringBuilder(title) : - new SpannableStringBuilder(Html.fromHtml( - context.getString(R.string.program_title_with_episode_number_no_season, - title, episodeNumber))); + builder = + TextUtils.isEmpty(episodeNumber) + ? new SpannableStringBuilder(title) + : new SpannableStringBuilder(Html.fromHtml(context.getString( + R.string.program_title_with_episode_number_no_season, + title, + episodeNumber))); } else { - builder = new SpannableStringBuilder(Html.fromHtml( - context.getString(R.string.program_title_with_episode_number, - title, seasonNumber, episodeNumber))); + builder = + new SpannableStringBuilder( + Html.fromHtml( + context.getString( + R.string.program_title_with_episode_number, + title, + seasonNumber, + episodeNumber))); } Object[] spans = builder.getSpans(0, builder.length(), Object.class); if (spans.length > 0) { if (episodeNumberStyleResId != 0) { - builder.setSpan(new TextAppearanceSpan(context, episodeNumberStyleResId), - builder.getSpanStart(spans[0]), builder.getSpanEnd(spans[0]), + builder.setSpan( + new TextAppearanceSpan(context, episodeNumberStyleResId), + builder.getSpanStart(spans[0]), + builder.getSpanEnd(spans[0]), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } builder.removeSpan(spans[0]); } return new SpannableString(builder); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java index 4f06ebcf..373daaaf 100644 --- a/src/com/android/tv/dvr/ui/FadeBackground.java +++ b/src/com/android/tv/dvr/ui/FadeBackground.java @@ -28,12 +28,9 @@ import android.transition.TransitionValues; import android.transition.Visibility; import android.util.AttributeSet; import android.view.ViewGroup; - import com.android.tv.R; -/** - * This transition fades in/out of the background of the view by changing the background color. - */ +/** This transition fades in/out of the background of the view by changing the background color. */ public class FadeBackground extends Transition { private final int mMode; @@ -45,22 +42,22 @@ public class FadeBackground extends Transition { } @Override - public void captureStartValues(TransitionValues transitionValues) { } + public void captureStartValues(TransitionValues transitionValues) {} @Override - public void captureEndValues(TransitionValues transitionValues) { } + public void captureEndValues(TransitionValues transitionValues) {} @Override - public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, - TransitionValues endValues) { + public Animator createAnimator( + ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { if (startValues == null || endValues == null) { return null; } Drawable background = endValues.view.getBackground(); if (background instanceof ColorDrawable) { int color = ((ColorDrawable) background).getColor(); - int transparentColor = Color.argb(0, Color.red(color), Color.green(color), - Color.blue(color)); + int transparentColor = + Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)); return mMode == Visibility.MODE_OUT ? ObjectAnimator.ofArgb(background, "color", transparentColor) : ObjectAnimator.ofArgb(background, "color", transparentColor, color); diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 8c0af9ed..1eb8080a 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -19,9 +19,7 @@ package com.android.tv.dvr.ui; import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PresenterSelector; - import com.android.tv.common.SoftPreconditions; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -45,8 +43,8 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { this(presenterSelector, comparator, Integer.MAX_VALUE); } - public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, - int maxItemCount) { + public SortedArrayAdapter( + PresenterSelector presenterSelector, Comparator<T> comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; @@ -88,9 +86,8 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * Adds an item in sorted order to the adapter. * * @param item The item to add in sorted order to the adapter. - * @param insertToEnd If items are inserted in a more or less sorted fashion, - * sets this parameter to {@code true} to search insertion position from - * the end to save search time. + * @param insertToEnd If items are inserted in a more or less sorted fashion, sets this + * parameter to {@code true} to search insertion position from the end to save search time. */ public final void add(T item, boolean insertToEnd) { long newItemId = getId(item); @@ -127,9 +124,7 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { return removeWithId((T) item); } - /** - * Removes an item which has the same ID as {@code item}. - */ + /** Removes an item which has the same ID as {@code item}. */ public boolean removeWithId(T item) { int index = indexWithId(item); return index >= 0 && index < size() && removeItems(index, 1) == 1; @@ -166,6 +161,7 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { /** * Changes an item in the list. + * * @param item The item to change. */ public final void change(T item) { @@ -181,9 +177,7 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { add(item); } - /** - * Checks whether the item is in the list. - */ + /** Checks whether the item is in the list. */ public final boolean contains(T item) { return indexWithId(item) != -1; } @@ -194,10 +188,10 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { } /** - * Returns the id of the the given {@code item}, which will be used in {@link #change} to - * decide if the given item is already existed in the adapter. + * Returns the id of the the given {@code item}, which will be used in {@link #change} to decide + * if the given item is already existed in the adapter. * - * The id must be stable. + * <p>The id must be stable. */ protected abstract long getId(T item); @@ -212,11 +206,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { return -1; } - /** - * Finds the position that the given item should be inserted to keep the sorted order. - */ + /** Finds the position that the given item should be inserted to keep the sorted order. */ public int findInsertPosition(T item) { - for (int i = size() - mExtraItemCount - 1; i >=0; i--) { + for (int i = size() - mExtraItemCount - 1; i >= 0; i--) { T r = (T) get(i); if (mComparator.compare(r, item) <= 0) { return i + 1; @@ -242,4 +234,4 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { } return lb; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java index 5fe9c478..0172f76f 100644 --- a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java @@ -19,8 +19,7 @@ package com.android.tv.dvr.ui; import android.content.Context; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidedAction; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; /** A {@link GuidedStepFragment} with {@link Tracker} for analytics. */ @@ -30,7 +29,7 @@ public abstract class TrackedGuidedStepFragment extends GuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mTracker = TvApplication.getSingletons(context).getAnalytics().getDefaultTracker(); + mTracker = TvSingletons.getSingletons(context).getAnalytics().getDefaultTracker(); } @Override diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java index 38a78f5d..f3a6fea4 100644 --- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -32,8 +32,8 @@ import android.widget.Button; class ActionPresenterSelector extends PresenterSelector { private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); - private final Presenter[] mPresenters = new Presenter[] { - mOneLineActionPresenter, mTwoLineActionPresenter}; + private final Presenter[] mPresenters = + new Presenter[] {mOneLineActionPresenter, mTwoLineActionPresenter}; @Override public Presenter getPresenter(Object item) { @@ -65,8 +65,9 @@ class ActionPresenterSelector extends PresenterSelector { class OneLineActionPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_1_line, parent, false); + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_1_line, parent, false); return new ActionViewHolder(v, parent.getLayoutDirection()); } @@ -87,8 +88,9 @@ class ActionPresenterSelector extends PresenterSelector { class TwoLineActionPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_2_lines, parent, false); + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_2_lines, parent, false); return new ActionViewHolder(v, parent.getLayoutDirection()); } @@ -100,14 +102,20 @@ class ActionPresenterSelector extends PresenterSelector { vh.mAction = action; if (icon != null) { - final int startPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); - final int endPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); + final int startPadding = + vh.view + .getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); + final int endPadding = + vh.view + .getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); } else { - final int padding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); + final int padding = + vh.view + .getResources() + .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); vh.view.setPaddingRelative(padding, 0, padding, 0); } vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); @@ -131,4 +139,4 @@ class ActionPresenterSelector extends PresenterSelector { vh.mAction = null; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java index bf18ddc0..7e7e1f75 100644 --- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -21,9 +21,8 @@ import android.content.res.Resources; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; @@ -31,9 +30,7 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; -/** - * {@link RecordingDetailsFragment} for current recording in DVR. - */ +/** {@link RecordingDetailsFragment} for current recording in DVR. */ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { private static final int ACTION_STOP_RECORDING = 1; @@ -41,7 +38,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = new DvrDataManager.ScheduledRecordingListener() { @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } + public void onScheduledRecordingAdded(ScheduledRecording... schedules) {} @Override public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { @@ -58,7 +55,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { for (ScheduledRecording schedule : schedules) { if (schedule.getId() == getRecording().getId() && schedule.getState() - != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { getActivity().finish(); return; } @@ -69,7 +66,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager(); mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); } @@ -78,9 +75,13 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); - adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, - res.getString(R.string.dvr_detail_stop_recording), null, - res.getDrawable(R.drawable.lb_ic_stop))); + adapter.set( + ACTION_STOP_RECORDING, + new Action( + ACTION_STOP_RECORDING, + res.getString(R.string.dvr_detail_stop_recording), + null, + res.getDrawable(R.drawable.lb_ic_stop))); return adapter; } @@ -90,7 +91,8 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { @Override public void onActionClicked(Action action) { if (action.getId() == ACTION_STOP_RECORDING) { - DvrUiHelper.showStopRecordingDialog(getActivity(), + DvrUiHelper.showStopRecordingDialog( + getActivity(), getRecording().getChannelId(), DvrStopRecordingFragment.REASON_USER_STOP, new HalfSizedDialogFragment.OnActionClickListener() { @@ -98,7 +100,7 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { public void onActionClick(long actionId) { if (actionId == DvrStopRecordingFragment.ACTION_STOP) { DvrManager dvrManager = - TvApplication.getSingletons(getContext()) + TvSingletons.getSingletons(getContext()) .getDvrManager(); dvrManager.stopRecording(getRecording()); getActivity().finish(); diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java index c1fa05d7..cba6293b 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -20,18 +20,15 @@ import android.content.Context; import android.media.tv.TvContract; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; -/** - * A class for details content. - */ +/** A class for details content. */ class DetailsContent { /** Constant for invalid time. */ public static final long INVALID_TIME = -1; @@ -44,8 +41,8 @@ class DetailsContent { private String mBackgroundImageUri; private boolean mUsingChannelLogo; - static DetailsContent createFromRecordedProgram(Context context, - RecordedProgram recordedProgram) { + static DetailsContent createFromRecordedProgram( + Context context, RecordedProgram recordedProgram) { return new DetailsContent.Builder() .setChannelId(recordedProgram.getChannelId()) .setProgramTitle(recordedProgram.getTitle()) @@ -53,32 +50,72 @@ class DetailsContent { .setEpisodeNumber(recordedProgram.getEpisodeNumber()) .setStartTimeUtcMillis(recordedProgram.getStartTimeUtcMillis()) .setEndTimeUtcMillis(recordedProgram.getEndTimeUtcMillis()) - .setDescription(TextUtils.isEmpty(recordedProgram.getLongDescription()) - ? recordedProgram.getDescription() : recordedProgram.getLongDescription()) + .setDescription( + TextUtils.isEmpty(recordedProgram.getLongDescription()) + ? recordedProgram.getDescription() + : recordedProgram.getLongDescription()) .setPosterArtUri(recordedProgram.getPosterArtUri()) .setThumbnailUri(recordedProgram.getThumbnailUri()) .build(context); } - static DetailsContent createFromSeriesRecording(Context context, - SeriesRecording seriesRecording) { + static DetailsContent createFromSeriesRecording( + Context context, SeriesRecording seriesRecording) { return new DetailsContent.Builder() .setChannelId(seriesRecording.getChannelId()) .setTitle(seriesRecording.getTitle()) - .setDescription(TextUtils.isEmpty(seriesRecording.getLongDescription()) - ? seriesRecording.getDescription() : seriesRecording.getLongDescription()) + .setDescription( + TextUtils.isEmpty(seriesRecording.getLongDescription()) + ? seriesRecording.getDescription() + : seriesRecording.getLongDescription()) .setPosterArtUri(seriesRecording.getPosterUri()) .setThumbnailUri(seriesRecording.getPhotoUri()) .build(context); } - static DetailsContent createFromScheduledRecording(Context context, - ScheduledRecording scheduledRecording) { - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(scheduledRecording.getChannelId()); - String description = !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) ? - scheduledRecording.getProgramDescription() - : scheduledRecording.getProgramLongDescription(); + static DetailsContent createFromScheduledRecording( + Context context, ScheduledRecording scheduledRecording) { + Channel channel = + TvSingletons.getSingletons(context) + .getChannelDataManager() + .getChannel(scheduledRecording.getChannelId()); + String description = + !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) + ? scheduledRecording.getProgramDescription() + : scheduledRecording.getProgramLongDescription(); + if (TextUtils.isEmpty(description)) { + description = channel != null ? channel.getDescription() : null; + } + return new DetailsContent.Builder() + .setChannelId(scheduledRecording.getChannelId()) + .setProgramTitle(scheduledRecording.getProgramTitle()) + .setSeasonNumber(scheduledRecording.getSeasonNumber()) + .setEpisodeNumber(scheduledRecording.getEpisodeNumber()) + .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs()) + .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs()) + .setDescription(description) + .setPosterArtUri(scheduledRecording.getProgramPosterArtUri()) + .setThumbnailUri(scheduledRecording.getProgramThumbnailUri()) + .build(context); + } + + static DetailsContent createFromFailedScheduledRecording( + Context context, ScheduledRecording scheduledRecording, String errMsg) { + Channel channel = + TvSingletons.getSingletons(context) + .getChannelDataManager() + .getChannel(scheduledRecording.getChannelId()); + String description; + if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED + && errMsg != null) { + description = errMsg + + " (Error code: " + scheduledRecording.getFailedReason() + ")"; + } else { + description = + !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) + ? scheduledRecording.getProgramDescription() + : scheduledRecording.getProgramLongDescription(); + } if (TextUtils.isEmpty(description)) { description = channel != null ? channel.getDescription() : null; } @@ -95,60 +132,44 @@ class DetailsContent { .build(context); } - private DetailsContent() { } + private DetailsContent() {} - /** - * Returns title. - */ + /** Returns title. */ public CharSequence getTitle() { return mTitle; } - /** - * Returns start time. - */ + /** Returns start time. */ public long getStartTimeUtcMillis() { return mStartTimeUtcMillis; } - /** - * Returns end time. - */ + /** Returns end time. */ public long getEndTimeUtcMillis() { return mEndTimeUtcMillis; } - /** - * Returns description. - */ + /** Returns description. */ public String getDescription() { return mDescription; } - /** - * Returns Logo image URI as a String. - */ + /** Returns Logo image URI as a String. */ public String getLogoImageUri() { return mLogoImageUri; } - /** - * Returns background image URI as a String. - */ + /** Returns background image URI as a String. */ public String getBackgroundImageUri() { return mBackgroundImageUri; } - /** - * Returns if image URIs are from its channels' logo. - */ + /** Returns if image URIs are from its channels' logo. */ public boolean isUsingChannelLogo() { return mUsingChannelLogo; } - /** - * Copies other details content. - */ + /** Copies other details content. */ public void copyFrom(DetailsContent other) { if (this == other) { return; @@ -162,9 +183,7 @@ class DetailsContent { mUsingChannelLogo = other.mUsingChannelLogo; } - /** - * A class for building details content. - */ + /** A class for building details content. */ public static final class Builder { private final DetailsContent mDetailsContent; @@ -181,49 +200,37 @@ class DetailsContent { mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; } - /** - * Sets title. - */ + /** Sets title. */ public Builder setTitle(CharSequence title) { mDetailsContent.mTitle = title; return this; } - /** - * Sets start time. - */ + /** Sets start time. */ public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; return this; } - /** - * Sets end time. - */ + /** Sets end time. */ public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; return this; } - /** - * Sets description. - */ + /** Sets description. */ public Builder setDescription(String description) { mDetailsContent.mDescription = description; return this; } - /** - * Sets logo image URI as a String. - */ + /** Sets logo image URI as a String. */ public Builder setLogoImageUri(String logoImageUri) { mDetailsContent.mLogoImageUri = logoImageUri; return this; } - /** - * Sets background image URI as a String. - */ + /** Sets background image URI as a String. */ public Builder setBackgroundImageUri(String backgroundImageUri) { mDetailsContent.mBackgroundImageUri = backgroundImageUri; return this; @@ -260,12 +267,18 @@ class DetailsContent { } private void createStyledTitle(Context context, Channel channel) { - CharSequence title = DvrUiHelper.getStyledTitleWithEpisodeNumber(context, - mProgramTitle, mSeasonNumber, mEpisodeNumber, - R.style.text_appearance_card_view_episode_number); + CharSequence title = + DvrUiHelper.getStyledTitleWithEpisodeNumber( + context, + mProgramTitle, + mSeasonNumber, + mEpisodeNumber, + R.style.text_appearance_card_view_episode_number); if (TextUtils.isEmpty(title)) { - mDetailsContent.mTitle = channel != null ? channel.getDisplayName() - : context.getResources().getString(R.string.no_program_information); + mDetailsContent.mTitle = + channel != null + ? channel.getDisplayName() + : context.getResources().getString(R.string.no_program_information); } else { mDetailsContent.mTitle = title; } @@ -288,20 +301,19 @@ class DetailsContent { mDetailsContent.mBackgroundImageUri = mThumbnailUri; } if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { - String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) - .toString(); + String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); mDetailsContent.mLogoImageUri = channelLogoUri; mDetailsContent.mBackgroundImageUri = channelLogoUri; mDetailsContent.mUsingChannelLogo = true; } } - /** - * Builds details content. - */ + /** Builds details content. */ public DetailsContent build(Context context) { - Channel channel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(mChannelId); + Channel channel = + TvSingletons.getSingletons(context) + .getChannelDataManager() + .getChannel(mChannelId); if (mDetailsContent.mTitle == null) { createStyledTitle(context, channel); } @@ -314,4 +326,4 @@ class DetailsContent { return detailsContent; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java index 09b57887..aec8c411 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -16,11 +16,11 @@ package com.android.tv.dvr.ui.browse; -import android.app.Activity; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.app.Activity; import android.content.Context; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; @@ -33,24 +33,20 @@ import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.ui.ViewUtils; import com.android.tv.util.Utils; /** - * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a - * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. - * Most codes of this class is originated from - * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. - * The latter class are re-used to provide a customized version of - * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. + * An {@link Presenter} for rendering a detailed description of an DVR item. Typically this + * Presenter will be used in a {@link + * android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. Most codes of this class is + * originated from {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. + * The latter class are re-used to provide a customized version of {@link + * android.support.v17.leanback.widget.DetailsOverviewRow}. */ class DetailsContentPresenter extends Presenter { - /** - * The ViewHolder for the {@link DetailsContentPresenter}. - */ + /** The ViewHolder for the {@link DetailsContentPresenter}. */ public static class ViewHolder extends Presenter.ViewHolder { final TextView mTitle; final TextView mSubtitle; @@ -85,31 +81,40 @@ class DetailsContentPresenter extends Presenter { return false; } final int bodyLines = mBody.getLineCount(); - int maxLines = mFullTextMode ? bodyLines : - (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); + int maxLines = + mFullTextMode + ? bodyLines + : (mTitle.getLineCount() > 1 + ? mBodyMinLines + : mBodyMaxLines); if (bodyLines > maxLines) { mReadMoreView.setVisibility(View.VISIBLE); mDescriptionContainer.setFocusable(true); mDescriptionContainer.setClickable(true); - mDescriptionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mFullTextMode = true; - mReadMoreView.setVisibility(View.GONE); - mDescriptionContainer.setFocusable(( - (AccessibilityManager) view.getContext() - .getSystemService( - Context.ACCESSIBILITY_SERVICE)) - .isEnabled()); - mDescriptionContainer.setClickable(false); - mDescriptionContainer.setOnClickListener(null); - int oldMaxLines = mBody.getMaxLines(); - mBody.setMaxLines(bodyLines); - // Minus 1 from line difference to eliminate the space - // originally occupied by "READ MORE" - showFullText((bodyLines - oldMaxLines - 1) * mBodyLineSpacing); - } - }); + mDescriptionContainer.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + mFullTextMode = true; + mReadMoreView.setVisibility(View.GONE); + mDescriptionContainer.setFocusable( + ((AccessibilityManager) + view.getContext() + .getSystemService( + Context + .ACCESSIBILITY_SERVICE)) + .isEnabled()); + mDescriptionContainer.setClickable(false); + mDescriptionContainer.setOnClickListener(null); + int oldMaxLines = mBody.getMaxLines(); + mBody.setMaxLines(bodyLines); + // Minus 1 from line difference to eliminate the space + // originally occupied by "READ MORE" + showFullText( + (bodyLines - oldMaxLines - 1) + * mBodyLineSpacing); + } + }); } if (mReadMoreView.getVisibility() == View.VISIBLE && mSubtitle.getVisibility() == View.VISIBLE) { @@ -151,30 +156,42 @@ class DetailsContentPresenter extends Presenter { // We have to explicitly set focusable to true here for accessibility, since we might // set the view's focusable state when we need to show "READ MORE", which would remove // the default focusable state for accessibility. - mDescriptionContainer.setFocusable(((AccessibilityManager) view.getContext() - .getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled()); + mDescriptionContainer.setFocusable( + ((AccessibilityManager) + view.getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE)) + .isEnabled()); mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); - final int titleAscent = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_baseline); + final int titleAscent = + view.getResources() + .getDimensionPixelSize(R.dimen.lb_details_description_title_baseline); // Ascent is negative mTitleMargin = titleAscent + titleFontMetricsInt.ascent; - mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_title_baseline_margin); - mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.dvr_details_description_under_subtitle_baseline_margin); + mUnderTitleBaselineMargin = + view.getResources() + .getDimensionPixelSize( + R.dimen.lb_details_description_under_title_baseline_margin); + mUnderSubtitleBaselineMargin = + view.getResources() + .getDimensionPixelSize( + R.dimen.dvr_details_description_under_subtitle_baseline_margin); - mTitleLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_line_spacing); - mBodyLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_body_line_spacing); + mTitleLineSpacing = + view.getResources() + .getDimensionPixelSize( + R.dimen.lb_details_description_title_line_spacing); + mBodyLineSpacing = + view.getResources() + .getDimensionPixelSize( + R.dimen.lb_details_description_body_line_spacing); - mBodyMaxLines = view.getResources().getInteger( - R.integer.lb_details_description_body_max_lines); - mBodyMinLines = view.getResources().getInteger( - R.integer.lb_details_description_body_min_lines); + mBodyMaxLines = + view.getResources().getInteger(R.integer.lb_details_description_body_max_lines); + mBodyMinLines = + view.getResources().getInteger(R.integer.lb_details_description_body_min_lines); mTitleMaxLines = mTitle.getMaxLines(); mTitleFontMetricsInt = getFontMetricsInt(mTitle); @@ -218,12 +235,14 @@ class DetailsContentPresenter extends Presenter { private void showFullText(int heightDiff) { final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); - Animator expandAnimator = ViewUtils.createHeightAnimator( - detailsFrame, nowHeight, nowHeight + heightDiff); + Animator expandAnimator = + ViewUtils.createHeightAnimator(detailsFrame, nowHeight, nowHeight + heightDiff); expandAnimator.setDuration(mFullTextAnimationDuration); - Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - 0f, -(heightDiff / 2))); + Animator shiftAnimator = + ObjectAnimator.ofPropertyValuesHolder( + detailsFrame, + PropertyValuesHolder.ofFloat( + View.TRANSLATION_Y, 0f, -(heightDiff / 2))); shiftAnimator.setDuration(mFullTextAnimationDuration); AnimatorSet fullTextAnimator = new AnimatorSet(); fullTextAnimator.playTogether(expandAnimator, shiftAnimator); @@ -237,14 +256,17 @@ class DetailsContentPresenter extends Presenter { public DetailsContentPresenter(Activity activity) { super(); mActivity = activity; - mFullTextAnimationDuration = mActivity.getResources() - .getInteger(R.integer.dvr_details_full_text_animation_duration); + mFullTextAnimationDuration = + mActivity + .getResources() + .getInteger(R.integer.dvr_details_full_text_animation_duration); } @Override public final ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.dvr_details_description, parent, false); + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.dvr_details_description, parent, false); return new ViewHolder(v); } @@ -263,8 +285,11 @@ class DetailsContentPresenter extends Presenter { } else { vh.mTitle.setText(detailsContent.getTitle()); vh.mTitle.setVisibility(View.VISIBLE); - vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() - + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); + vh.mTitle.setLineSpacing( + vh.mTitleLineSpacing + - vh.mTitle.getLineHeight() + + vh.mTitle.getLineSpacingExtra(), + vh.mTitle.getLineSpacingMultiplier()); vh.mTitle.setMaxLines(vh.mTitleMaxLines); } setTopMargin(vh.mTitle, vh.mTitleMargin); @@ -272,13 +297,19 @@ class DetailsContentPresenter extends Presenter { boolean hasSubtitle = true; if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { - vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), - detailsContent.getStartTimeUtcMillis(), - detailsContent.getEndTimeUtcMillis(), false)); + vh.mSubtitle.setText( + Utils.getDurationString( + viewHolder.view.getContext(), + detailsContent.getStartTimeUtcMillis(), + detailsContent.getEndTimeUtcMillis(), + false)); vh.mSubtitle.setVisibility(View.VISIBLE); if (hasTitle) { - setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin - + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); + setTopMargin( + vh.mSubtitle, + vh.mUnderTitleBaselineMargin + + vh.mSubtitleFontMetricsInt.ascent + - vh.mTitleFontMetricsInt.descent); } else { setTopMargin(vh.mSubtitle, 0); } @@ -292,16 +323,23 @@ class DetailsContentPresenter extends Presenter { } else { vh.mBody.setText(detailsContent.getDescription()); vh.mBody.setVisibility(View.VISIBLE); - vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() - + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); + vh.mBody.setLineSpacing( + vh.mBodyLineSpacing - vh.mBody.getLineHeight() + vh.mBody.getLineSpacingExtra(), + vh.mBody.getLineSpacingMultiplier()); if (hasSubtitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); + setTopMargin( + vh.mDescriptionContainer, + vh.mUnderSubtitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent + - vh.mSubtitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); } else if (hasTitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); + setTopMargin( + vh.mDescriptionContainer, + vh.mUnderTitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent + - vh.mTitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); } else { setTopMargin(vh.mDescriptionContainer, 0); } @@ -309,11 +347,11 @@ class DetailsContentPresenter extends Presenter { } @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {} private void setTopMargin(View view, int topMargin) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); lp.topMargin = topMargin; view.setLayoutParams(lp); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java index 82fe9ce3..849360b8 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -23,9 +23,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.support.v17.leanback.app.BackgroundManager; -/** - * The Background Helper. - */ +/** The Background Helper. */ class DetailsViewBackgroundHelper { // Background delay serves to avoid kicking off expensive bitmap loading // in case multiple backgrounds are set in quick succession. @@ -59,11 +57,10 @@ class DetailsViewBackgroundHelper { public DetailsViewBackgroundHelper(Activity activity) { mBackgroundManager = BackgroundManager.getInstance(activity); mBackgroundManager.attach(activity.getWindow()); + mBackgroundManager.setAutoReleaseOnStop(false); } - /** - * Sets the given image to background. - */ + /** Sets the given image to background. */ public void setBackground(Drawable background) { if (mRunnable != null) { mHandler.removeCallbacks(mRunnable); @@ -72,18 +69,14 @@ class DetailsViewBackgroundHelper { mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS); } - /** - * Sets the background color. - */ + /** Sets the background color. */ public void setBackgroundColor(int color) { if (mBackgroundManager.isAttached()) { mBackgroundManager.setColor(color); } } - /** - * Sets the background scrim. - */ + /** Sets the background scrim. */ public void setScrim(int color) { if (mBackgroundManager.isAttached()) { mBackgroundManager.setDimLayer(new ColorDrawable(color)); diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java index 07eec107..6cc1c7a1 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -20,19 +20,16 @@ import android.app.Activity; import android.content.Intent; import android.media.tv.TvInputManager; import android.os.Bundle; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; -/** - * {@link android.app.Activity} for DVR UI. - */ +/** {@link android.app.Activity} for DVR UI. */ public class DvrBrowseActivity extends Activity { private DvrBrowseFragment mFragment; @Override public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); super.onCreate(savedInstanceState); setContentView(R.layout.dvr_main); mFragment = (DvrBrowseFragment) getFragmentManager().findFragmentById(R.id.dvr_frame); @@ -49,4 +46,4 @@ public class DvrBrowseActivity extends Activity { mFragment.showScheduledRow(); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java index cb3a5745..40b3a1f0 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -16,7 +16,9 @@ package com.android.tv.dvr.ui.browse; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.BrowseFragment; @@ -30,9 +32,9 @@ import android.util.Log; import android.view.View; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.data.GenreItems; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; @@ -52,12 +54,15 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; -/** - * {@link BrowseFragment} for DVR functions. - */ -public class DvrBrowseFragment extends BrowseFragment implements - RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener, - OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener { +/** {@link BrowseFragment} for DVR functions. */ +@TargetApi(Build.VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated +public class DvrBrowseFragment extends BrowseFragment + implements RecordedProgramListener, + ScheduledRecordingListener, + SeriesRecordingListener, + OnDvrScheduleLoadFinishedListener, + OnRecordedProgramLoadFinishedListener { private static final String TAG = "DvrBrowseFragment"; private static final boolean DEBUG = false; @@ -67,7 +72,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private boolean mShouldShowScheduleRow; private boolean mEntranceTransitionEnded; - private RecordedProgramAdapter mRecentAdapter; + private RecentRowAdapter mRecentAdapter; private ScheduleAdapter mScheduleAdapter; private SeriesAdapter mSeriesAdapter; private RecordedProgramAdapter[] mGenreAdapters = @@ -98,82 +103,143 @@ public class DvrBrowseFragment extends BrowseFragment implements } }; - private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof SeriesRecording) { - lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); - } - if (rhs instanceof SeriesRecording) { - rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); - } - if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else { - return -1; + private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = + new Comparator<Object>() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof SeriesRecording) { + lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); + } + if (rhs instanceof SeriesRecording) { + rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); + } + if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else { + return -1; + } + } else if (rhs instanceof RecordedProgram) { + return 1; + } else { + return 0; + } } - } else if (rhs instanceof RecordedProgram) { - return 1; - } else { - return 0; - } - } - }; + }; - private static final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else { - return -1; + private static final Comparator<Object> SCHEDULE_COMPARATOR = + new Comparator<Object>() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else { + return -1; + } + } else if (rhs instanceof ScheduledRecording) { + return 1; + } else { + return 0; + } } - } else if (rhs instanceof ScheduledRecording) { - return 1; - } else { - return 0; - } - } - }; + }; + + static final Comparator<Object> RECENT_ROW_COMPARATOR = + new Comparator<Object>() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .reversed() + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else if (rhs instanceof RecordedProgram) { + ScheduledRecording scheduled = (ScheduledRecording) lhs; + RecordedProgram recorded = (RecordedProgram) rhs; + int compare = + Long.compare( + recorded.getStartTimeUtcMillis(), + scheduled.getStartTimeMs()); + // recorded program first when the start times are the same + return compare == 0 ? 1 : compare; + } else { + return -1; + } + } else if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else if (rhs instanceof ScheduledRecording) { + RecordedProgram recorded = (RecordedProgram) lhs; + ScheduledRecording scheduled = (ScheduledRecording) rhs; + int compare = + Long.compare( + scheduled.getStartTimeMs(), + recorded.getStartTimeUtcMillis()); + // recorded program first when the start times are the same + return compare == 0 ? -1 : compare; + } else { + return -1; + } + } else { + return !(rhs instanceof RecordedProgram) + && !(rhs instanceof ScheduledRecording) + ? 0 : 1; + } + } + }; private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = new DvrScheduleManager.OnConflictStateChangeListener() { - @Override - public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { - if (mScheduleAdapter != null) { - for (ScheduledRecording schedule : schedules) { - onScheduledRecordingConflictStatusChanged(schedule); + @Override + public void onConflictStateChange( + boolean conflict, ScheduledRecording... schedules) { + if (mScheduleAdapter != null) { + for (ScheduledRecording schedule : schedules) { + onScheduledRecordingConflictStatusChanged(schedule); + } + } } - } - } - }; + }; - private final Runnable mUpdateRowsRunnable = new Runnable() { - @Override - public void run() { - updateRows(); - } - }; + private final Runnable mUpdateRowsRunnable = + new Runnable() { + @Override + public void run() { + updateRows(); + } + }; @Override public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); Context context = getContext(); - ApplicationSingletons singletons = TvApplication.getSingletons(context); + TvSingletons singletons = TvSingletons.getSingletons(context); mDvrDataManager = singletons.getDvrDataManager(); mDvrScheudleManager = singletons.getDvrScheduleManager(); - mPresenterSelector = new ClassPresenterSelector() - .addClassPresenter(ScheduledRecording.class, - new ScheduledRecordingPresenter(context)) - .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) - .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, - new FullSchedulesCardPresenter(context)); + mPresenterSelector = + new ClassPresenterSelector() + .addClassPresenter( + ScheduledRecording.class, new ScheduledRecordingPresenter(context)) + .addClassPresenter( + RecordedProgram.class, new RecordedProgramPresenter(context)) + .addClassPresenter( + SeriesRecording.class, new SeriesRecordingPresenter(context)) + .addClassPresenter( + FullScheduleCardHolder.class, + new FullSchedulesCardPresenter(context)); + + if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) { + mPresenterSelector.addClassPresenter( + DvrHistoryCardHolder.class, + new DvrHistoryCardPresenter(context)); + } mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); prepareUiElements(); @@ -195,7 +261,8 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDestroyView() { - getView().getViewTreeObserver() + getView() + .getViewTreeObserver() .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); super.onDestroyView(); } @@ -263,6 +330,8 @@ public class DvrBrowseFragment extends BrowseFragment implements for (ScheduledRecording scheduleRecording : scheduledRecordings) { if (needToShowScheduledRecording(scheduleRecording)) { mScheduleAdapter.add(scheduleRecording); + } else if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + mRecentAdapter.add(scheduleRecording); } } } @@ -361,30 +430,44 @@ public class DvrBrowseFragment extends BrowseFragment implements private boolean startBrowseIfDvrInitialized() { if (mDvrDataManager.isInitialized()) { // Setup rows - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); + mRecentAdapter = new RecentRowAdapter(MAX_RECENT_ITEM_COUNT); mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); mSeriesAdapter = new SeriesAdapter(); for (int i = 0; i < mGenreAdapters.length; i++) { mGenreAdapters[i] = new RecordedProgramAdapter(); } // Schedule Recordings. - List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); + // only get not started or in progress recordings + List<ScheduledRecording> schedules = mDvrDataManager.getAvailableScheduledRecordings(); onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); // Recorded Programs. for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { handleRecordedProgramAdded(recordedProgram, false); } + if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) { + // only get failed recordings + for (ScheduledRecording scheduledRecording + : mDvrDataManager.getFailedScheduledRecordings()) { + onScheduledRecordingAdded(scheduledRecording); + } + mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER); + } // Series Recordings. Series recordings should be added after recorded programs, because - // we build series recordings' latest program information while adding recorded programs. + // we build series recordings' latest program information while adding recorded + // programs. List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); handleSeriesRecordingsAdded(recordings); - mRecentRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_recent)), mRecentAdapter); - mScheduledRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_scheduled)), mScheduleAdapter); - mSeriesRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_series)), mSeriesAdapter); + mRecentRow = + new ListRow( + new HeaderItem(getString(R.string.dvr_main_recent)), mRecentAdapter); + mScheduledRow = + new ListRow( + new HeaderItem(getString(R.string.dvr_main_scheduled)), + mScheduleAdapter); + mSeriesRow = + new ListRow( + new HeaderItem(getString(R.string.dvr_main_series)), mSeriesAdapter); mRowsAdapter.add(mScheduledRow); updateRows(); // Initialize listeners @@ -398,16 +481,18 @@ public class DvrBrowseFragment extends BrowseFragment implements return false; } - private void handleRecordedProgramAdded(RecordedProgram recordedProgram, - boolean updateSeriesRecording) { + private void handleRecordedProgramAdded( + RecordedProgram recordedProgram, boolean updateSeriesRecording) { mRecentAdapter.add(recordedProgram); String seriesId = recordedProgram.getSeriesId(); SeriesRecording seriesRecording = null; if (seriesId != null) { seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) < 0) { + if (latestProgram == null + || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare( + latestProgram, recordedProgram) + < 0) { mSeriesId2LatestProgram.put(seriesId, recordedProgram); if (updateSeriesRecording && seriesRecording != null) { onSeriesRecordingChanged(seriesRecording); @@ -415,8 +500,8 @@ public class DvrBrowseFragment extends BrowseFragment implements } } if (seriesRecording == null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + for (RecordedProgramAdapter adapter : + getGenreAdapters(recordedProgram.getCanonicalGenres())) { adapter.add(recordedProgram); } } @@ -436,8 +521,8 @@ public class DvrBrowseFragment extends BrowseFragment implements } } } - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + for (RecordedProgramAdapter adapter : + getGenreAdapters(recordedProgram.getCanonicalGenres())) { adapter.remove(recordedProgram); } } @@ -449,8 +534,10 @@ public class DvrBrowseFragment extends BrowseFragment implements if (seriesId != null) { seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) <= 0) { + if (latestProgram == null + || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare( + latestProgram, recordedProgram) + <= 0) { mSeriesId2LatestProgram.put(seriesId, recordedProgram); if (seriesRecording != null) { onSeriesRecordingChanged(seriesRecording); @@ -463,8 +550,8 @@ public class DvrBrowseFragment extends BrowseFragment implements } } if (seriesRecording == null) { - updateGenreAdapters(getGenreAdapters( - recordedProgram.getCanonicalGenres()), recordedProgram); + updateGenreAdapters( + getGenreAdapters(recordedProgram.getCanonicalGenres()), recordedProgram); } else { updateGenreAdapters(new ArrayList<>(), recordedProgram); } @@ -474,8 +561,8 @@ public class DvrBrowseFragment extends BrowseFragment implements for (SeriesRecording seriesRecording : seriesRecordings) { mSeriesAdapter.add(seriesRecording); if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + for (RecordedProgramAdapter adapter : + getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { adapter.add(seriesRecording); } } @@ -485,8 +572,8 @@ public class DvrBrowseFragment extends BrowseFragment implements private void handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { mSeriesAdapter.remove(seriesRecording); - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + for (RecordedProgramAdapter adapter : + getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { adapter.remove(seriesRecording); } } @@ -496,8 +583,8 @@ public class DvrBrowseFragment extends BrowseFragment implements for (SeriesRecording seriesRecording : seriesRecordings) { mSeriesAdapter.change(seriesRecording); if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - updateGenreAdapters(getGenreAdapters( - seriesRecording.getCanonicalGenreIds()), seriesRecording); + updateGenreAdapters( + getGenreAdapters(seriesRecording.getCanonicalGenreIds()), seriesRecording); } else { // Remove series recording from all genre rows if it has no recorded program updateGenreAdapters(new ArrayList<>(), seriesRecording); @@ -512,7 +599,7 @@ public class DvrBrowseFragment extends BrowseFragment implements } else { for (String genre : genres) { int genreId = GenreItems.getId(genre); - if(genreId >= mGenreAdapters.length) { + if (genreId >= mGenreAdapters.length) { Log.d(TAG, "Wrong Genre ID: " + genreId); } else { result.add(mGenreAdapters[genreId]); @@ -528,7 +615,7 @@ public class DvrBrowseFragment extends BrowseFragment implements result.add(mGenreAdapters[mGenreAdapters.length - 1]); } else { for (int genreId : genreIds) { - if(genreId >= mGenreAdapters.length) { + if (genreId >= mGenreAdapters.length) { Log.d(TAG, "Wrong Genre ID: " + genreId); } else { result.add(mGenreAdapters[genreId]); @@ -554,8 +641,9 @@ public class DvrBrowseFragment extends BrowseFragment implements } private void updateRows() { - int visibleRowsCount = 1; // Schedule's Row will never be empty - if (mRecentAdapter.isEmpty()) { + int visibleRowsCount = 1; // Schedule's Row will never be empty + int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0; + if (mRecentAdapter.size() <= recentRowMinSize) { mRowsAdapter.remove(mRecentRow); } else { if (mRowsAdapter.indexOf(mRecentRow) < 0) { @@ -597,8 +685,9 @@ public class DvrBrowseFragment extends BrowseFragment implements RecordedProgram latestProgram = null; for (RecordedProgram program : mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { - if (latestProgram == null || RecordedProgram - .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { + if (latestProgram == null + || RecordedProgram.START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) + < 0) { latestProgram = program; } } @@ -622,17 +711,19 @@ public class DvrBrowseFragment extends BrowseFragment implements private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> { SeriesAdapter() { - super(mPresenterSelector, new Comparator<SeriesRecording>() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - if (lhs.isStopped() && !rhs.isStopped()) { - return 1; - } else if (!lhs.isStopped() && rhs.isStopped()) { - return -1; - } - return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); - } - }); + super( + mPresenterSelector, + new Comparator<SeriesRecording>() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; + } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + } + }); } @Override @@ -662,4 +753,22 @@ public class DvrBrowseFragment extends BrowseFragment implements } } } -}
\ No newline at end of file + + private class RecentRowAdapter extends SortedArrayAdapter<Object> { + RecentRowAdapter(int maxItemCount) { + super(mPresenterSelector, RECENT_ROW_COMPARATOR, maxItemCount); + } + + @Override + public long getId(Object item) { + // We takes the inverse number for the ID of scheduled recordings to make the ID stable. + if (item instanceof ScheduledRecording) { + return -((ScheduledRecording) item).getId() - 1; + } else if (item instanceof RecordedProgram) { + return ((RecordedProgram) item).getId(); + } else { + return -1; + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java index 35d21db8..0336b319 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java @@ -19,21 +19,16 @@ package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; - import android.transition.Transition; import android.transition.Transition.TransitionListener; import android.view.View; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; import com.android.tv.dialog.PinDialogFragment; -/** - * Activity to show details view in DVR. - */ +/** Activity to show details view in DVR. */ public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener { - /** - * Name of record id added to the Intent. - */ + /** Name of record id added to the Intent. */ public static final String RECORDING_ID = "record_id"; /** @@ -42,46 +37,38 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On */ public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; - /** - * Name of details view's type added to the intent. - */ + /** Name of details view's type added to the intent. */ public static final String DETAILS_VIEW_TYPE = "details_view_type"; - /** - * Name of shared element between activities. - */ + /** Name of shared element between activities. */ public static final String SHARED_ELEMENT_NAME = "shared_element"; - /** - * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. - */ + /** Name of error message of a failed recording */ + public static final String EXTRA_FAILED_MESSAGE = "failed_message"; + + /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */ public static final int CURRENT_RECORDING_VIEW = 1; - /** - * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. - */ + /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */ public static final int SCHEDULED_RECORDING_VIEW = 2; - /** - * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. - */ + /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */ public static final int RECORDED_PROGRAM_VIEW = 3; - /** - * SERIES_RECORDING_VIEW refers to series recording in DVR. - */ + /** SERIES_RECORDING_VIEW refers to series recording in DVR. */ public static final int SERIES_RECORDING_VIEW = 4; private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener; @Override public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_details); long recordId = getIntent().getLongExtra(RECORDING_ID, -1); int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); + String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE); if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { Bundle args = new Bundle(); args.putLong(RECORDING_ID, recordId); @@ -90,6 +77,7 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On detailsFragment = new CurrentRecordingDetailsFragment(); } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); + args.putString(EXTRA_FAILED_MESSAGE, failedMsg); detailsFragment = new ScheduledRecordingDetailsFragment(); } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { detailsFragment = new RecordedProgramDetailsFragment(); @@ -97,8 +85,10 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On detailsFragment = new SeriesRecordingDetailsFragment(); } detailsFragment.setArguments(args); - getFragmentManager().beginTransaction() - .replace(R.id.dvr_details_view_frame, detailsFragment).commit(); + getFragmentManager() + .beginTransaction() + .replace(R.id.dvr_details_view_frame, detailsFragment) + .commit(); } // This is a workaround for the focus on O device diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java index 19fb7117..8f4e4dab 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -36,21 +36,19 @@ import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.support.v17.leanback.widget.VerticalGridView; import android.text.TextUtils; import android.widget.Toast; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.ImageLoader; import com.android.tv.util.ToastUtils; -import com.android.tv.util.Utils; - +import com.android.tv.util.images.ImageLoader; import java.io.File; abstract class DvrDetailsFragment extends DetailsFragment { @@ -77,8 +75,8 @@ abstract class DvrDetailsFragment extends DetailsFragment { public void onStart() { super.onStart(); // TODO: remove the workaround of b/30401180. - VerticalGridView container = (VerticalGridView) getActivity() - .findViewById(R.id.container_list); + VerticalGridView container = + (VerticalGridView) getActivity().findViewById(R.id.container_list); // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. container.setItemAlignmentOffset(0); container.setWindowAlignmentOffset( @@ -86,27 +84,23 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void setupAdapter() { - DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( - new DetailsContentPresenter(getActivity())); - rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, - null)); - rowPresenter.setSharedElementEnterTransition(getActivity(), - DvrDetailsActivity.SHARED_ELEMENT_NAME); + DetailsOverviewRowPresenter rowPresenter = + new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity())); + rowPresenter.setBackgroundColor( + getResources().getColor(R.color.common_tv_background, null)); + rowPresenter.setSharedElementEnterTransition( + getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME); rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); setAdapter(mRowsAdapter); } - /** - * Returns details views' rows adapter. - */ + /** Returns details views' rows adapter. */ protected ArrayObjectAdapter getRowsAdapter() { - return mRowsAdapter; + return mRowsAdapter; } - /** - * Sets details overview. - */ + /** Sets details overview. */ protected void setDetailsOverviewRow(DetailsContent detailsContent) { mDetailsOverview = new DetailsOverviewRow(detailsContent); mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); @@ -114,9 +108,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { onLoadLogoAndBackgroundImages(detailsContent); } - /** - * Creates and returns presenter selector will be used by rows adaptor. - */ + /** Creates and returns presenter selector will be used by rows adaptor. */ protected PresenterSelector onCreatePresenterSelector( DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); @@ -130,11 +122,9 @@ abstract class DvrDetailsFragment extends DetailsFragment { * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to * do after the super class did onCreate, it should override this method and put the codes here. */ - protected void onCreateInternal() { } + protected void onCreateInternal() {} - /** - * Updates actions of details overview. - */ + /** Updates actions of details overview. */ protected void updateActions() { mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); } @@ -142,14 +132,12 @@ abstract class DvrDetailsFragment extends DetailsFragment { /** * Loads recording details according to the arguments the fragment got. * - * @return false if cannot find valid recordings, else return true. If the return value - * is false, the detail activity and fragment will be ended. + * @return false if cannot find valid recordings, else return true. If the return value is + * false, the detail activity and fragment will be ended. */ abstract boolean onLoadRecordingDetails(Bundle args); - /** - * Creates actions users can interact with and their adaptor for this fragment. - */ + /** Creates actions users can interact with and their adaptor for this fragment. */ abstract SparseArrayObjectAdapter onCreateActionsAdapter(); /** @@ -158,66 +146,76 @@ abstract class DvrDetailsFragment extends DetailsFragment { */ abstract OnActionClickedListener onCreateOnActionClickedListener(); - /** - * Loads logo and background images for detail fragments. - */ + /** Loads logo and background images for detail fragments. */ protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { Drawable logoDrawable = null; Drawable backgroundDrawable = null; if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { - logoDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); + logoDrawable = + getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); mDetailsOverview.setImageDrawable(logoDrawable); } if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { - backgroundDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); + backgroundDrawable = + getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); mBackgroundHelper.setBackground(backgroundDrawable); } if (logoDrawable != null && backgroundDrawable != null) { return; } - if (logoDrawable == null && backgroundDrawable == null - && detailsContent.getLogoImageUri().equals( - detailsContent.getBackgroundImageUri())) { - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, - getContext())); + if (logoDrawable == null + && backgroundDrawable == null + && detailsContent + .getLogoImageUri() + .equals(detailsContent.getBackgroundImageUri())) { + ImageLoader.loadBitmap( + getContext(), + detailsContent.getLogoImageUri(), + new MyImageLoaderCallback( + this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext())); return; } if (logoDrawable == null) { int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); - int imageHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_details_poster_height); - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - imageWidth, imageHeight, + int imageHeight = + getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height); + ImageLoader.loadBitmap( + getContext(), + detailsContent.getLogoImageUri(), + imageWidth, + imageHeight, new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); } if (backgroundDrawable == null) { - ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), + ImageLoader.loadBitmap( + getContext(), + detailsContent.getBackgroundImageUri(), new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); } } protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { - if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && - !isDataUriAccessible(recordedProgram.getDataUri())) { + if (CommonUtils.isInBundledPackageSet(recordedProgram.getPackageName()) + && !isDataUriAccessible(recordedProgram.getDataUri())) { // Since cleaning RecordedProgram from forgotten storage will take some time, // ignore playback until cleaning is finished. - ToastUtils.show(getContext(), + ToastUtils.show( + getContext(), getContext().getResources().getString(R.string.dvr_toast_recording_deleted), Toast.LENGTH_SHORT); return; } long programId = recordedProgram.getId(); - ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) - .getTvInputManagerHelper().getParentalControlSettings(); + ParentalControlSettings parental = + TvSingletons.getSingletons(getActivity()) + .getTvInputManagerHelper() + .getParentalControlSettings(); if (!parental.isParentalControlsEnabled()) { DvrUiHelper.startPlaybackActivity(getContext(), programId, seekTimeMs, false); return; } ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); + TvSingletons.getSingletons(getActivity()).getChannelDataManager(); Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); if (channel != null && channel.isLocked()) { checkPinToPlay(recordedProgram, seekTimeMs); @@ -249,36 +247,43 @@ abstract class DvrDetailsFragment extends DetailsFragment { private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity); if (getActivity() instanceof DvrDetailsActivity) { - ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(new OnPinCheckedListener() { - @Override - public void onPinChecked(boolean checked, int type, String rating) { - ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(null); - if (checked && type == PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) { - DvrUiHelper.startPlaybackActivity(getContext(), recordedProgram.getId(), - seekTimeMs, true); - } - } - }); + ((DvrDetailsActivity) getActivity()) + .setOnPinCheckListener( + new OnPinCheckedListener() { + @Override + public void onPinChecked(boolean checked, int type, String rating) { + ((DvrDetailsActivity) getActivity()) + .setOnPinCheckListener(null); + if (checked + && type + == PinDialogFragment + .PIN_DIALOG_TYPE_UNLOCK_PROGRAM) { + DvrUiHelper.startPlaybackActivity( + getContext(), + recordedProgram.getId(), + seekTimeMs, + true); + } + } + }); PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); } } - private static class MyImageLoaderCallback extends - ImageLoader.ImageLoaderCallback<DvrDetailsFragment> { + private static class MyImageLoaderCallback + extends ImageLoader.ImageLoaderCallback<DvrDetailsFragment> { private final Context mContext; private final int mLoadType; - public MyImageLoaderCallback(DvrDetailsFragment fragment, - int loadType, Context context) { + public MyImageLoaderCallback(DvrDetailsFragment fragment, int loadType, Context context) { super(fragment); mLoadType = loadType; mContext = context; } @Override - public void onBitmapLoaded(DvrDetailsFragment fragment, - @Nullable Bitmap bitmap) { + public void onBitmapLoaded(DvrDetailsFragment fragment, @Nullable Bitmap bitmap) { Drawable drawable; int loadType = mLoadType; if (bitmap == null) { diff --git a/src/com/android/tv/config/ConfigKeys.java b/src/com/android/tv/dvr/ui/browse/DvrHistoryCardHolder.java index 7df033d2..c6288ef0 100644 --- a/src/com/android/tv/config/ConfigKeys.java +++ b/src/com/android/tv/dvr/ui/browse/DvrHistoryCardHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,12 @@ * limitations under the License. */ -package com.android.tv.config; - -/** - * Static list of config keys. - */ -public final class ConfigKeys { +package com.android.tv.dvr.ui.browse; +/** Special object for schedule preview; */ +final class DvrHistoryCardHolder { + /** Full schedule card holder. */ + static final DvrHistoryCardHolder DVR_HISTORY_CARD_HOLDER = new DvrHistoryCardHolder(); - private ConfigKeys() { - } + private DvrHistoryCardHolder() {} } diff --git a/src/com/android/tv/dvr/ui/browse/DvrHistoryCardPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrHistoryCardPresenter.java new file mode 100644 index 00000000..62c050c9 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrHistoryCardPresenter.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.View; +import com.android.tv.R; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** Presents a DVR history card view in the {@link DvrBrowseFragment}. */ +class DvrHistoryCardPresenter extends DvrItemPresenter<Object> { + private final Drawable mIconDrawable; + private final String mCardTitleText; + + DvrHistoryCardPresenter(Context context) { + super(context); + mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule); + mCardTitleText = mContext.getString(R.string.dvr_history_card_view_title); + } + + @Override + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new DvrItemViewHolder(new RecordingCardView(mContext)); + } + + @Override + public void onBindDvrItemViewHolder(DvrItemViewHolder vh, Object o) { + final RecordingCardView cardView = (RecordingCardView) vh.view; + + cardView.setTitle(mCardTitleText); + cardView.setImage(mIconDrawable); + } + + @Override + public void onUnbindViewHolder(ViewHolder vh) { + ((RecordingCardView) vh.view).reset(); + super.onUnbindViewHolder(vh); + } + + @Override + protected View.OnClickListener onCreateOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View view) { + DvrUiHelper.startDvrHistoryActivity(mContext); + } + }; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java index df0e61c1..4298d86a 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java @@ -23,18 +23,15 @@ import android.support.v17.leanback.widget.Presenter; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; - import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.ui.DvrUiHelper; - import java.util.HashSet; import java.util.Set; /** * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: - * {@link com.android.tv.dvr.data.ScheduledRecording}, - * {@link com.android.tv.dvr.data.RecordedProgram}, and + * {@link DvrBrowseFragment}. DVR items might include: {@link + * com.android.tv.dvr.data.ScheduledRecording}, {@link com.android.tv.dvr.data.RecordedProgram}, and * {@link com.android.tv.dvr.data.SeriesRecording}. */ public abstract class DvrItemPresenter<T> extends Presenter { @@ -51,9 +48,9 @@ public abstract class DvrItemPresenter<T> extends Presenter { return (RecordingCardView) view; } - protected void onBound(T item) { } + protected void onBound(T item) {} - protected void onUnbound() { } + protected void onUnbound() {} } DvrItemPresenter(Context context) { @@ -94,9 +91,7 @@ public abstract class DvrItemPresenter<T> extends Presenter { viewHolder.view.setOnClickListener(null); } - /** - * Unbinds all bound view holders. - */ + /** Unbinds all bound view holders. */ public void unbindAllViewHolders() { // When browse fragments are destroyed, RecyclerView would not call presenters' // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. @@ -105,36 +100,28 @@ public abstract class DvrItemPresenter<T> extends Presenter { } } - /** - * This method will be called when a {@link DvrItemViewHolder} is needed to be created. - */ - abstract protected DvrItemViewHolder onCreateDvrItemViewHolder(); + /** This method will be called when a {@link DvrItemViewHolder} is needed to be created. */ + protected abstract DvrItemViewHolder onCreateDvrItemViewHolder(); - /** - * This method will be called when a {@link DvrItemViewHolder} is bound to a DVR item. - */ - abstract protected void onBindDvrItemViewHolder(DvrItemViewHolder viewHolder, T item); + /** This method will be called when a {@link DvrItemViewHolder} is bound to a DVR item. */ + protected abstract void onBindDvrItemViewHolder(DvrItemViewHolder viewHolder, T item); - /** - * Returns context. - */ + /** Returns context. */ protected Context getContext() { return mContext; } - /** - * Creates {@link OnClickListener} for DVR library's card views. - */ + /** Creates {@link OnClickListener} for DVR library's card views. */ protected OnClickListener onCreateOnClickListener() { return new OnClickListener() { @Override public void onClick(View view) { if (view instanceof RecordingCardView) { RecordingCardView v = (RecordingCardView) view; - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), - v.getTag(), v.getImageView(), false); + DvrUiHelper.startDetailsActivity( + (Activity) v.getContext(), v.getTag(), v.getImageView(), false); } } }; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java index 37a72eaf..a2d1cb28 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java @@ -19,7 +19,6 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.support.v17.leanback.widget.ListRowPresenter; import android.view.ViewGroup; - import com.android.tv.R; /** A list row presenter to display expand/fold card views list. */ diff --git a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java index 311137a9..6def818f 100644 --- a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java +++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java @@ -16,14 +16,10 @@ package com.android.tv.dvr.ui.browse; -/** - * Special object for schedule preview; - */ +/** Special object for schedule preview; */ final class FullScheduleCardHolder { - /** - * Full schedule card holder. - */ + /** Full schedule card holder. */ static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder(); - private FullScheduleCardHolder() { } + private FullScheduleCardHolder() {} } diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java index 94c67eec..af0f24c0 100644 --- a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java @@ -19,20 +19,15 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.View; -import android.view.ViewGroup; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; - import java.util.Collections; import java.util.List; -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ +/** Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ class FullSchedulesCardPresenter extends DvrItemPresenter<Object> { private final Drawable mIconDrawable; private final String mCardTitleText; @@ -54,16 +49,26 @@ class FullSchedulesCardPresenter extends DvrItemPresenter<Object> { cardView.setTitle(mCardTitleText); cardView.setImage(mIconDrawable); - List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext) - .getDvrDataManager().getAvailableScheduledRecordings(); + List<ScheduledRecording> scheduledRecordings = + TvSingletons.getSingletons(mContext) + .getDvrDataManager() + .getAvailableScheduledRecordings(); int fullDays = 0; if (!scheduledRecordings.isEmpty()) { - fullDays = Utils.computeDateDifference(System.currentTimeMillis(), - Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) - .getStartTimeMs()) + 1; + fullDays = + Utils.computeDateDifference( + System.currentTimeMillis(), + Collections.max( + scheduledRecordings, + ScheduledRecording.START_TIME_COMPARATOR) + .getStartTimeMs()) + + 1; } - cardView.setContent(mContext.getResources().getQuantityString( - R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); + cardView.setContent( + mContext.getResources() + .getQuantityString( + R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), + null); } @Override @@ -81,4 +86,4 @@ class FullSchedulesCardPresenter extends DvrItemPresenter<Object> { } }; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java index eb9cb26c..47b1a198 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -22,17 +22,14 @@ import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.data.RecordedProgram; -/** - * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. - */ +/** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment implements DvrDataManager.RecordedProgramListener { private static final int ACTION_RESUME_PLAYING = 1; @@ -47,17 +44,17 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment @Override public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + mDvrDataManager = TvSingletons.getSingletons(getContext()).getDvrDataManager(); mDvrDataManager.addRecordedProgramListener(this); super.onCreate(savedInstanceState); } @Override public void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); - setDetailsOverviewRow(DetailsContent - .createFromRecordedProgram(getContext(), mRecordedProgram)); + mDvrWatchedPositionManager = + TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager(); + setDetailsOverviewRow( + DetailsContent.createFromRecordedProgram(getContext(), mRecordedProgram)); } @Override @@ -95,20 +92,36 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment Resources res = getResources(); if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, - res.getString(R.string.dvr_detail_resume_play), null, - res.getDrawable(R.drawable.lb_ic_play))); - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_play_from_beginning), null, - res.getDrawable(R.drawable.lb_ic_replay))); + adapter.set( + ACTION_RESUME_PLAYING, + new Action( + ACTION_RESUME_PLAYING, + res.getString(R.string.dvr_detail_resume_play), + null, + res.getDrawable(R.drawable.lb_ic_play))); + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_play_from_beginning), + null, + res.getDrawable(R.drawable.lb_ic_replay))); } else { - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_watch), null, - res.getDrawable(R.drawable.lb_ic_play))); + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_watch), + null, + res.getDrawable(R.drawable.lb_ic_play))); } - adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING, - res.getString(R.string.dvr_detail_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp))); + adapter.set( + ACTION_DELETE_RECORDING, + new Action( + ACTION_DELETE_RECORDING, + res.getString(R.string.dvr_detail_delete), + null, + res.getDrawable(R.drawable.ic_delete_32dp))); return adapter; } @@ -120,11 +133,13 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); } else if (action.getId() == ACTION_RESUME_PLAYING) { - startPlayback(mRecordedProgram, mDvrWatchedPositionManager - .getWatchedPosition(mRecordedProgram.getId())); + startPlayback( + mRecordedProgram, + mDvrWatchedPositionManager.getWatchedPosition( + mRecordedProgram.getId())); } else if (action.getId() == ACTION_DELETE_RECORDING) { - DvrManager dvrManager = TvApplication - .getSingletons(getActivity()).getDvrManager(); + DvrManager dvrManager = + TvSingletons.getSingletons(getActivity()).getDvrManager(); dvrManager.removeRecordedProgram(mRecordedProgram); getActivity().finish(); } @@ -133,10 +148,10 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment } @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {} @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {} @Override public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java index 5fe162b6..e2db3ac4 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java @@ -18,17 +18,14 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.media.tv.TvInputManager; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.Utils; -/** - * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. - */ +/** Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. */ public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram> { private final DvrWatchedPositionManager mDvrWatchedPositionManager; private String mTodayString; @@ -53,10 +50,16 @@ public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram> } private void setProgressBar(long watchedPositionMs) { - ((RecordingCardView) view).setProgressBar( - (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null - : Math.min(100, (int) (100.0f * watchedPositionMs - / mProgram.getDurationMillis()))); + ((RecordingCardView) view) + .setProgressBar( + (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) + ? null + : Math.min( + 100, + (int) + (100.0f + * watchedPositionMs + / mProgram.getDurationMillis()))); } @Override @@ -86,15 +89,15 @@ public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram> } } - RecordedProgramPresenter(Context context, boolean showEpisodeTitle, - boolean expandTitleWhenFocused) { + RecordedProgramPresenter( + Context context, boolean showEpisodeTitle, boolean expandTitleWhenFocused) { super(context); mTodayString = mContext.getString(R.string.dvr_date_today); mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); mDvrWatchedPositionManager = - TvApplication.getSingletons(mContext).getDvrWatchedPositionManager(); - mProgressBarColor = mContext.getResources() - .getColor(R.color.play_controls_progress_bar_watched); + TvSingletons.getSingletons(mContext).getDvrWatchedPositionManager(); + mProgressBarColor = + mContext.getResources().getColor(R.color.play_controls_progress_bar_watched); mShowEpisodeTitle = showEpisodeTitle; mExpandTitleWhenFocused = expandTitleWhenFocused; } @@ -114,29 +117,37 @@ public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram> final RecordedProgramViewHolder viewHolder = (RecordedProgramViewHolder) baseHolder; final RecordingCardView cardView = viewHolder.getView(); DetailsContent details = DetailsContent.createFromRecordedProgram(mContext, program); - cardView.setTitle(mShowEpisodeTitle ? - program.getEpisodeDisplayTitle(mContext) : details.getTitle()); + cardView.setTitle( + mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) : details.getTitle()); cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); cardView.setContent(generateMajorContent(program), generateMinorContent(program)); cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); } private String generateMajorContent(RecordedProgram program) { - int dateDifference = Utils.computeDateDifference(program.getStartTimeUtcMillis(), - System.currentTimeMillis()); + int dateDifference = + Utils.computeDateDifference( + program.getStartTimeUtcMillis(), System.currentTimeMillis()); if (dateDifference == 0) { return mTodayString; } else if (dateDifference == 1) { return mYesterdayString; } else { - return Utils.getDurationString(mContext, program.getStartTimeUtcMillis(), - program.getStartTimeUtcMillis(), false, true, false, 0); + return Utils.getDurationString( + mContext, + program.getStartTimeUtcMillis(), + program.getStartTimeUtcMillis(), + false, + true, + false, + 0); } } private String generateMinorContent(RecordedProgram program) { int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); - return mContext.getResources().getQuantityString( - R.plurals.dvr_program_duration, durationMinutes, durationMinutes); + return mContext.getResources() + .getQuantityString( + R.plurals.dvr_program_duration, durationMinutes, durationMinutes); } } diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java index 767addc8..fe3c52d9 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -31,20 +31,19 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.ui.ViewUtils; -import com.android.tv.util.ImageLoader; +import com.android.tv.util.images.ImageLoader; /** - * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} - * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. + * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} or + * {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. */ public class RecordingCardView extends BaseCardView { // This value should be the same with // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS - private final static int ANIMATION_DURATION = 150; + private static final int ANIMATION_DURATION = 150; private final ImageView mImageView; private final int mImageWidth; private final int mImageHeight; @@ -70,16 +69,19 @@ public class RecordingCardView extends BaseCardView { } public RecordingCardView(Context context, boolean expandTitleWhenFocused) { - this(context, context.getResources().getDimensionPixelSize( - R.dimen.dvr_library_card_image_layout_width), context.getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), + this( + context, + context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_width), + context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), expandTitleWhenFocused); } - public RecordingCardView(Context context, int imageWidth, int imageHeight, - boolean expandTitleWhenFocused) { + public RecordingCardView( + Context context, int imageWidth, int imageHeight, boolean expandTitleWhenFocused) { super(context); - //TODO(dvr): move these to the layout XML. + // TODO(dvr): move these to the layout XML. setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); setFocusable(true); @@ -99,21 +101,27 @@ public class RecordingCardView extends BaseCardView { mTitleArea = (FrameLayout) findViewById(R.id.title_area); mFoldedTitleView = (TextView) findViewById(R.id.title_one_line); mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines); - mFoldedTitleHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); - mExpandedTitleHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); + mFoldedTitleHeight = + getResources().getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); + mExpandedTitleHeight = + getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION); - mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - float value = (Float) valueAnimator.getAnimatedValue(); - mExpandedTitleView.setAlpha(value); - mFoldedTitleView.setAlpha(1.0f - value); - ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight - + (mExpandedTitleHeight - mFoldedTitleHeight) * value)); - } - }); + mExpandTitleAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float value = (Float) valueAnimator.getAnimatedValue(); + mExpandedTitleView.setAlpha(value); + mFoldedTitleView.setAlpha(1.0f - value); + ViewUtils.setLayoutHeight( + mTitleArea, + (int) + (mFoldedTitleHeight + + (mExpandedTitleHeight - mFoldedTitleHeight) + * value)); + } + }); mExpandTitleWhenFocused = expandTitleWhenFocused; } @@ -124,8 +132,12 @@ public class RecordingCardView extends BaseCardView { // loading and drawing background images during activity transitions. if (gainFocus) { if (!TextUtils.isEmpty(mDetailBackgroundImageUri)) { - ImageLoader.loadBitmap(getContext(), mDetailBackgroundImageUri, - Integer.MAX_VALUE, Integer.MAX_VALUE, null); + ImageLoader.loadBitmap( + getContext(), + mDetailBackgroundImageUri, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + null); } } if (mExpandTitleWhenFocused) { @@ -186,9 +198,7 @@ public class RecordingCardView extends BaseCardView { } } - /** - * Sets progress bar. If progress is {@code null}, hides progress bar. - */ + /** Sets progress bar. If progress is {@code null}, hides progress bar. */ void setProgressBar(Integer progress) { if (progress == null) { mProgressBar.setVisibility(View.GONE); @@ -198,16 +208,14 @@ public class RecordingCardView extends BaseCardView { } } - /** - * Sets the color of progress bar. - */ + /** Sets the color of progress bar. */ void setProgressBarColor(int color) { mProgressBar.getProgressDrawable().setTint(color); } /** * Sets the image URI of the poster should be shown on the card view. - + * * @param isChannelLogo {@code true} if the image is from channels' logo. */ void setImageUri(String uri, boolean isChannelLogo) { @@ -220,14 +228,16 @@ public class RecordingCardView extends BaseCardView { if (TextUtils.isEmpty(uri)) { mImageView.setImageDrawable(mDefaultImage); } else { - ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, + ImageLoader.loadBitmap( + getContext(), + uri, + mImageWidth, + mImageHeight, new RecordingCardImageLoaderCallback(this, uri)); } } - /** - * Sets the {@link Drawable} of the poster should be shown on the card view. - */ + /** Sets the {@link Drawable} of the poster should be shown on the card view. */ public void setImage(Drawable image) { if (image != null) { mImageView.setImageDrawable(image); @@ -255,9 +265,7 @@ public class RecordingCardView extends BaseCardView { mDetailBackgroundImageUri = uri; } - /** - * Returns image view. - */ + /** Returns image view. */ public ImageView getImageView() { return mImageView; } @@ -287,4 +295,4 @@ public class RecordingCardView extends BaseCardView { setContent(null, null); mImageView.setImageDrawable(mDefaultImage); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java index 56ec357f..aa2ccf75 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -18,34 +18,35 @@ package com.android.tv.dvr.ui.browse; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.data.ScheduledRecording; -/** - * {@link DetailsFragment} for recordings in DVR. - */ +/** {@link DetailsFragment} for recordings in DVR. */ abstract class RecordingDetailsFragment extends DvrDetailsFragment { private ScheduledRecording mRecording; @Override protected void onCreateInternal() { - setDetailsOverviewRow(DetailsContent - .createFromScheduledRecording(getContext(), mRecording)); + setDetailsOverviewRow( + DetailsContent.createFromScheduledRecording(getContext(), mRecording)); } @Override protected boolean onLoadRecordingDetails(Bundle args) { long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() - .getScheduledRecording(scheduledRecordingId); + mRecording = + TvSingletons.getSingletons(getContext()) + .getDvrDataManager() + .getScheduledRecording(scheduledRecordingId); return mRecording != null; } - /** - * Returns {@link ScheduledRecording} for the current fragment. - */ + protected ScheduledRecording getScheduledRecording() { + return mRecording; + } + + /** Returns {@link ScheduledRecording} for the current fragment. */ public ScheduledRecording getRecording() { return mRecording; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java index 958f8bf8..302b8318 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -21,16 +21,12 @@ import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ui.DvrUiHelper; -/** - * {@link RecordingDetailsFragment} for scheduled recording in DVR. - */ +/** {@link RecordingDetailsFragment} for scheduled recording in DVR. */ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { private static final int ACTION_VIEW_SCHEDULE = 1; private static final int ACTION_CANCEL = 2; @@ -38,11 +34,14 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment private DvrManager mDvrManager; private Action mScheduleAction; private boolean mHideViewSchedule; + private String mFailedMessage; @Override public void onCreate(Bundle savedInstance) { - mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); + Bundle args = getArguments(); + mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); + mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); + mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE); super.onCreate(savedInstance); } @@ -55,19 +54,37 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment } @Override + protected void onCreateInternal() { + if (mFailedMessage == null) { + super.onCreateInternal(); + return; + } + setDetailsOverviewRow( + DetailsContent.createFromFailedScheduledRecording( + getContext(), getScheduledRecording(), mFailedMessage)); + } + + @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); if (!mHideViewSchedule) { - mScheduleAction = new Action(ACTION_VIEW_SCHEDULE, - res.getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(getScheduleIconId())); + mScheduleAction = + new Action( + ACTION_VIEW_SCHEDULE, + res.getString(R.string.dvr_detail_view_schedule), + null, + res.getDrawable(getScheduleIconId())); adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); } - adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, - res.getString(R.string.dvr_detail_cancel_recording), null, - res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); + adapter.set( + ACTION_CANCEL, + new Action( + ACTION_CANCEL, + res.getString(R.string.dvr_detail_cancel_recording), + null, + res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); return adapter; } diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java index 273d3d19..8e028689 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -18,18 +18,14 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.os.Handler; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; - import java.util.concurrent.TimeUnit; -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ +/** Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); @@ -39,13 +35,14 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { private final class ScheduledRecordingViewHolder extends DvrItemViewHolder { private final Handler mHandler = new Handler(); private ScheduledRecording mScheduledRecording; - private final Runnable mProgressBarUpdater = new Runnable() { - @Override - public void run() { - updateProgressBar(); - mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); - } - }; + private final Runnable mProgressBarUpdater = + new Runnable() { + @Override + public void run() { + updateProgressBar(); + mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); + } + }; ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { super(view); @@ -73,9 +70,17 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { int recordingState = mScheduledRecording.getState(); RecordingCardView cardView = (RecordingCardView) view; if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - cardView.setProgressBar(Math.max(0, Math.min((int) (100 * - (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) - / mScheduledRecording.getDuration()), 100))); + cardView.setProgressBar( + Math.max( + 0, + Math.min( + (int) + (100 + * (System.currentTimeMillis() + - mScheduledRecording + .getStartTimeMs()) + / mScheduledRecording.getDuration()), + 100))); } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { cardView.setProgressBar(100); } else { @@ -95,9 +100,10 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { public ScheduledRecordingPresenter(Context context) { super(context); - mDvrManager = TvApplication.getSingletons(mContext).getDvrManager(); - mProgressBarColor = mContext.getResources() - .getColor(R.color.play_controls_recording_icon_color_on_focus); + mDvrManager = TvSingletons.getSingletons(mContext).getDvrManager(); + mProgressBarColor = + mContext.getResources() + .getColor(R.color.play_controls_recording_icon_color_on_focus); } @Override @@ -106,33 +112,61 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { } @Override - public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, - ScheduledRecording recording) { + public void onBindDvrItemViewHolder( + DvrItemViewHolder baseHolder, ScheduledRecording recording) { final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; final RecordingCardView cardView = viewHolder.getView(); DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording); cardView.setTitle(details.getTitle()); cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); - cardView.setAffiliatedIcon(mDvrManager.isConflicting(recording) ? - R.drawable.ic_warning_white_32dp : 0); + if (mDvrManager.isConflicting(recording)) { + cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); + } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp); + } else { + cardView.setAffiliatedIcon(0); + } cardView.setContent(generateMajorContent(recording), null); cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); } private String generateMajorContent(ScheduledRecording recording) { - int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), - recording.getStartTimeMs()); + if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + return mContext.getString(R.string.dvr_recording_failed); + } + int dateDifference = + Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs()); if (dateDifference <= 0) { - return mContext.getString(R.string.dvr_date_today_time, - Utils.getDurationString(mContext, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)); + return mContext.getString( + R.string.dvr_date_today_time, + Utils.getDurationString( + mContext, + recording.getStartTimeMs(), + recording.getEndTimeMs(), + false, + false, + true, + 0)); } else if (dateDifference == 1) { - return mContext.getString(R.string.dvr_date_tomorrow_time, - Utils.getDurationString(mContext, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)); + return mContext.getString( + R.string.dvr_date_tomorrow_time, + Utils.getDurationString( + mContext, + recording.getStartTimeMs(), + recording.getEndTimeMs(), + false, + false, + true, + 0)); } else { - return Utils.getDurationString(mContext, recording.getStartTimeMs(), - recording.getStartTimeMs(), false, true, false, 0); + return Utils.getDurationString( + mContext, + recording.getStartTimeMs(), + recording.getStartTimeMs(), + false, + true, + false, + 0); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java index c2aa8e98..2cd191a7 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -32,9 +32,8 @@ import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.text.TextUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.BaseProgram; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrWatchedPositionManager; @@ -42,16 +41,13 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.SortedArrayAdapter; - import java.util.Collections; import java.util.Comparator; import java.util.List; -/** - * {@link DetailsFragment} for series recording in DVR. - */ -public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements - DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { +/** {@link DetailsFragment} for series recording in DVR. */ +public class SeriesRecordingDetailsFragment extends DvrDetailsFragment + implements DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { private static final int ACTION_WATCH = 1; private static final int ACTION_SERIES_SCHEDULES = 2; private static final int ACTION_DELETE = 3; @@ -77,7 +73,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement @Override public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); mWatchLabel = getString(R.string.dvr_detail_watch); mResumeLabel = getString(R.string.dvr_detail_series_resume); mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); @@ -87,8 +83,8 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement @Override protected void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); + mDvrWatchedPositionManager = + TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager(); setDetailsOverviewRow(DetailsContent.createFromSeriesRecording(getContext(), mSeries)); setupRecordedProgramsRow(); mDvrDataManager.addSeriesRecordingListener(this); @@ -119,27 +115,31 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mActionsAdapter.clear(ACTION_WATCH); } else { String episodeStatus; - if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) + if (mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { episodeStatus = mResumeLabel; - mInitialPlaybackPositionMs = mDvrWatchedPositionManager - .getWatchedPosition(mRecommendRecordedProgram.getId()); + mInitialPlaybackPositionMs = + mDvrWatchedPositionManager.getWatchedPosition( + mRecommendRecordedProgram.getId()); } else { episodeStatus = mWatchLabel; mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; } - String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( - getContext()); - mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, - episodeStatus, episodeDisplayNumber, mWatchDrawable)); + String episodeDisplayNumber = + mRecommendRecordedProgram.getEpisodeDisplayNumber(getContext()); + mActionsAdapter.set( + ACTION_WATCH, + new Action(ACTION_WATCH, episodeStatus, episodeDisplayNumber, mWatchDrawable)); } } @Override protected boolean onLoadRecordingDetails(Bundle args) { long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() - .getSeriesRecording(recordId); + mSeries = + TvSingletons.getSingletons(getActivity()) + .getDvrDataManager() + .getSeriesRecording(recordId); if (mSeries == null) { return false; } @@ -162,12 +162,19 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); updateWatchAction(); - mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, - getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(R.drawable.ic_schedule_32dp, null))); - mDeleteAction = new Action(ACTION_DELETE, - getString(R.string.dvr_detail_series_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp, null)); + mActionsAdapter.set( + ACTION_SERIES_SCHEDULES, + new Action( + ACTION_SERIES_SCHEDULES, + getString(R.string.dvr_detail_view_schedule), + null, + res.getDrawable(R.drawable.ic_schedule_32dp, null))); + mDeleteAction = + new Action( + ACTION_DELETE, + getString(R.string.dvr_detail_series_delete), + null, + res.getDrawable(R.drawable.ic_delete_32dp, null)); if (!mRecordedPrograms.isEmpty()) { mActionsAdapter.set(ACTION_DELETE, mDeleteAction); } @@ -207,11 +214,9 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement }; } - /** - * The programs are sorted by season number and episode number. - */ + /** The programs are sorted by season number and episode number. */ private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { - for (int i = programs.size() - 1 ; i >= 0 ; i--) { + for (int i = programs.size() - 1; i >= 0; i--) { RecordedProgram program = programs.get(i); int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { @@ -230,7 +235,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement } @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {} @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { @@ -308,8 +313,10 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { Object row = rowsAdaptor.get(i); if (row instanceof ListRow) { - int compareResult = BaseProgram.numberCompare(seasonNumber, - ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); + int compareResult = + BaseProgram.numberCompare( + seasonNumber, + ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); if (compareResult == 0) { return (ListRow) row; } else if (compareResult < 0) { @@ -321,18 +328,25 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement } private ListRow createNewSeasonRow(String seasonNumber, int position) { - String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle() - : getString(R.string.dvr_detail_series_season_title, seasonNumber); + String seasonTitle = + seasonNumber.isEmpty() + ? mSeries.getTitle() + : getString(R.string.dvr_detail_series_season_title, seasonNumber); HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); ClassPresenterSelector selector = new ClassPresenterSelector(); selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); - ListRow row = new ListRow(header, new SeasonRowAdapter(selector, - new Comparator<RecordedProgram>() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); - } - }, seasonNumber)); + ListRow row = + new ListRow( + header, + new SeasonRowAdapter( + selector, + new Comparator<RecordedProgram>() { + @Override + public int compare(RecordedProgram lhs, RecordedProgram rhs) { + return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); + } + }, + seasonNumber)); getRowsAdapter().add(position, row); return row; } @@ -340,7 +354,9 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement private class SeasonRowAdapter extends SortedArrayAdapter<RecordedProgram> { private String mSeasonNumber; - SeasonRowAdapter(PresenterSelector selector, Comparator<RecordedProgram> comparator, + SeasonRowAdapter( + PresenterSelector selector, + Comparator<RecordedProgram> comparator, String seasonNumber) { super(selector, comparator); mSeasonNumber = seasonNumber; @@ -351,4 +367,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return program.getId(); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java index e508259d..14f9dceb 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java @@ -19,10 +19,8 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.media.tv.TvInputManager; import android.text.TextUtils; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; @@ -32,27 +30,29 @@ import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListen import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; - import java.util.List; -/** - * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. - */ +/** Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { private final DvrDataManager mDvrDataManager; private final DvrManager mDvrManager; private final DvrWatchedPositionManager mWatchedPositionManager; - private final class SeriesRecordingViewHolder extends DvrItemViewHolder implements - WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { + private final class SeriesRecordingViewHolder extends DvrItemViewHolder + implements WatchedPositionChangedListener, + ScheduledRecordingListener, + RecordedProgramListener { private SeriesRecording mSeriesRecording; private RecordingCardView mCardView; private DvrDataManager mDvrDataManager; private DvrManager mDvrManager; private DvrWatchedPositionManager mWatchedPositionManager; - SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, - DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { + SeriesRecordingViewHolder( + RecordingCardView view, + DvrDataManager dvrDataManager, + DvrManager dvrManager, + DvrWatchedPositionManager watchedPositionManager) { super(view); mCardView = view; mDvrDataManager = dvrDataManager; @@ -92,8 +92,8 @@ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { boolean needToUpdateCardView = false; for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { + if (TextUtils.equals( + recordedProgram.getSeriesId(), mSeriesRecording.getSeriesId())) { mDvrDataManager.removeScheduledRecordingListener(this); mWatchedPositionManager.addListener(this, recordedProgram.getId()); needToUpdateCardView = true; @@ -108,8 +108,8 @@ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { boolean needToUpdateCardView = false; for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { + if (TextUtils.equals( + recordedProgram.getSeriesId(), mSeriesRecording.getSeriesId())) { if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) == TvInputManager.TIME_SHIFT_INVALID_TIME) { mWatchedPositionManager.removeListener(this, recordedProgram.getId()); @@ -177,14 +177,15 @@ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { quantityStringID = R.plurals.dvr_count_new_recordings; } } - mCardView.setContent(mCardView.getResources() - .getQuantityString(quantityStringID, count, count), null); + mCardView.setContent( + mCardView.getResources().getQuantityString(quantityStringID, count, count), + null); } } public SeriesRecordingPresenter(Context context) { super(context); - ApplicationSingletons singletons = TvApplication.getSingletons(context); + TvSingletons singletons = TvSingletons.getSingletons(context); mDvrDataManager = singletons.getDvrDataManager(); mDvrManager = singletons.getDvrManager(); mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); @@ -192,8 +193,11 @@ class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { @Override public DvrItemViewHolder onCreateDvrItemViewHolder() { - return new SeriesRecordingViewHolder(new RecordingCardView(mContext), mDvrDataManager, - mDvrManager, mWatchedPositionManager); + return new SeriesRecordingViewHolder( + new RecordingCardView(mContext), + mDvrDataManager, + mDvrManager, + mWatchedPositionManager); } @Override diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index b9407b15..77a63508 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -1,18 +1,18 @@ /* -* 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 -*/ + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ package com.android.tv.dvr.ui.list; @@ -23,23 +23,17 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.data.ScheduledRecording; -/** - * A base fragment to show the list of schedule recordings. - */ +/** A base fragment to show the list of schedule recordings. */ public abstract class BaseDvrSchedulesFragment extends DetailsFragment implements DvrDataManager.ScheduledRecordingListener, - DvrScheduleManager.OnConflictStateChangeListener { - /** - * The key for scheduled recording which has be selected in the list. - */ + DvrScheduleManager.OnConflictStateChangeListener { + /** The key for scheduled recording which has be selected in the list. */ public static final String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording"; @@ -55,15 +49,15 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment mRowsAdapter = onCreateRowsAdapter(presenterSelector); setAdapter(mRowsAdapter); mRowsAdapter.start(); - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); singletons.getDvrDataManager().addScheduledRecordingListener(this); singletons.getDvrScheduleManager().addOnConflictStateChangeListener(this); mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); int firstItemPosition = getFirstItemPosition(); if (firstItemPosition != -1) { @@ -72,16 +66,12 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment return view; } - /** - * Returns rows adapter. - */ + /** Returns rows adapter. */ protected ScheduleRowAdapter getRowsAdapter() { return mRowsAdapter; } - /** - * Shows the empty message. - */ + /** Shows the empty message. */ void showEmptyMessage(int messageId) { mEmptyInfoScreenView.setText(messageId); if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) { @@ -89,9 +79,7 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment } } - /** - * Hides the empty message. - */ + /** Hides the empty message. */ void hideEmptyMessage() { if (mEmptyInfoScreenView.getVisibility() == View.VISIBLE) { mEmptyInfoScreenView.setVisibility(View.GONE); @@ -99,39 +87,32 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment } @Override - public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent, - Bundle savedInstanceState) { + public View onInflateTitleView( + LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { // Workaround of b/31046014 return null; } @Override public void onDestroy() { - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); singletons.getDvrScheduleManager().removeOnConflictStateChangeListener(this); singletons.getDvrDataManager().removeScheduledRecordingListener(this); mRowsAdapter.stop(); super.onDestroy(); } - /** - * Creates header row presenter. - */ + /** Creates header row presenter. */ public abstract SchedulesHeaderRowPresenter onCreateHeaderRowPresenter(); - /** - * Creates rows presenter. - */ + /** Creates rows presenter. */ public abstract ScheduleRowPresenter onCreateRowPresenter(); - /** - * Creates rows adapter. - */ - public abstract ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor); + /** Creates rows adapter. */ + public abstract ScheduleRowAdapter onCreateRowsAdapter( + ClassPresenterSelector presenterSelector); - /** - * Gets the first focus position in schedules list. - */ + /** Gets the first focus position in schedules list. */ protected int getFirstItemPosition() { for (int i = 0; i < mRowsAdapter.size(); i++) { if (mRowsAdapter.get(i) instanceof ScheduleRow) { @@ -176,4 +157,4 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryActivity.java b/src/com/android/tv/dvr/ui/list/DvrHistoryActivity.java new file mode 100644 index 00000000..623975e1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/DvrHistoryActivity.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.app.Activity; +import android.os.Bundle; +import com.android.tv.R; +import com.android.tv.Starter; + +/** Activity to show the recording history. */ +public class DvrHistoryActivity extends Activity { + + @Override + public void onCreate(final Bundle savedInstanceState) { + Starter.start(this); + // Pass null to prevent automatically re-creating fragments + super.onCreate(null); + setContentView(R.layout.activity_dvr_history); + DvrHistoryFragment dvrHistoryFragment = new DvrHistoryFragment(); + getFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, dvrHistoryFragment) + .commit(); + } +} diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java new file mode 100644 index 00000000..0ca05fac --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.tv.R; +import com.android.tv.TvSingletons; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; + +/** A fragment to show the DVR history. */ +public class DvrHistoryFragment extends DetailsFragment + implements DvrDataManager.ScheduledRecordingListener, + DvrDataManager.RecordedProgramListener { + + private DvrHistoryRowAdapter mRowsAdapter; + private TextView mEmptyInfoScreenView; + private DvrDataManager mDvrDataManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter( + SchedulesHeaderRow.class, new DateHeaderRowPresenter(getContext())); + presenterSelector.addClassPresenter( + ScheduleRow.class, new ScheduleRowPresenter(getContext())); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); + mRowsAdapter = new DvrHistoryRowAdapter( + getContext(), presenterSelector, singletons.getClock()); + setAdapter(mRowsAdapter); + mRowsAdapter.start(); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addRecordedProgramListener(this); + mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen); + } + + @Override + public void onDestroy() { + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrDataManager.removeRecordedProgramListener(this); + super.onDestroy(); + } + + /** Shows the empty message. */ + void showEmptyMessage() { + mEmptyInfoScreenView.setText(R.string.dvr_history_empty_state); + if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.VISIBLE); + } + } + + /** Hides the empty message. */ + void hideEmptyMessage() { + if (mEmptyInfoScreenView.getVisibility() == View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.GONE); + } + } + + @Override + public View onInflateTitleView( + LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { + // Workaround of b/31046014 + return null; + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { + mRowsAdapter.onScheduledRecordingAdded(recording); + } + if (mRowsAdapter.size() > 0) { + hideEmptyMessage(); + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { + mRowsAdapter.onScheduledRecordingRemoved(recording); + } + if (mRowsAdapter.size() == 0) { + showEmptyMessage(); + } + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { + mRowsAdapter.onScheduledRecordingUpdated(recording); + } + if (mRowsAdapter.size() == 0) { + showEmptyMessage(); + } else { + hideEmptyMessage(); + } + } + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + if (mRowsAdapter != null) { + for (RecordedProgram p : recordedPrograms) { + mRowsAdapter.onScheduledRecordingAdded(p); + } + if (mRowsAdapter.size() > 0) { + hideEmptyMessage(); + } + } + + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + if (mRowsAdapter != null) { + for (RecordedProgram program : recordedPrograms) { + mRowsAdapter.onScheduledRecordingUpdated(program); + } + if (mRowsAdapter.size() == 0) { + showEmptyMessage(); + } else { + hideEmptyMessage(); + } + } + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + if (mRowsAdapter != null) { + for (RecordedProgram p : recordedPrograms) { + mRowsAdapter.onScheduledRecordingRemoved(p); + } + if (mRowsAdapter.size() == 0) { + showEmptyMessage(); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java new file mode 100644 index 00000000..156d1a7e --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.text.format.DateUtils; +import android.util.Log; +import com.android.tv.R; +import com.android.tv.TvSingletons; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.Clock; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.recorder.ScheduledProgramReaper; +import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; +import com.android.tv.util.Utils; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** An adapter for DVR history. */ +@TargetApi(VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated +class DvrHistoryRowAdapter extends ArrayObjectAdapter { + private static final String TAG = "DvrHistoryRowAdapter"; + private static final boolean DEBUG = false; + + private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final int MAX_HISTORY_DAYS = ScheduledProgramReaper.DAYS; + + private final Context mContext; + private final Clock mClock; + private final DvrDataManager mDvrDataManager; + private final List<String> mTitles = new ArrayList<>(); + private final Map<Long, ScheduledRecording> mRecordedProgramScheduleMap = new HashMap<>(); + + public DvrHistoryRowAdapter( + Context context, ClassPresenterSelector classPresenterSelector, Clock clock) { + super(classPresenterSelector); + mContext = context; + mClock = clock; + mDvrDataManager = TvSingletons.getSingletons(mContext).getDvrDataManager(); + mTitles.add(mContext.getString(R.string.dvr_date_today)); + mTitles.add(mContext.getString(R.string.dvr_date_yesterday)); + } + + /** Returns context. */ + protected Context getContext() { + return mContext; + } + + /** Starts row adapter. */ + public void start() { + clear(); + List<ScheduledRecording> recordingList = mDvrDataManager.getFailedScheduledRecordings(); + List<RecordedProgram> recordedProgramList = mDvrDataManager.getRecordedPrograms(); + + recordingList.addAll( + recordedProgramsToScheduledRecordings(recordedProgramList, MAX_HISTORY_DAYS)); + recordingList + .sort(ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed()); + long deadLine = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()); + for (int i = 0; i < recordingList.size(); ) { + ArrayList<ScheduledRecording> section = new ArrayList<>(); + while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() >= deadLine) { + section.add(recordingList.get(i++)); + } + if (!section.isEmpty()) { + SchedulesHeaderRow headerRow = + new DateHeaderRow( + calculateHeaderDate(deadLine), + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + section.size(), + section.size()), + section.size(), + deadLine); + add(headerRow); + for (ScheduledRecording recording : section) { + add(new ScheduleRow(recording, headerRow)); + } + } + deadLine -= ONE_DAY_MS; + } + } + + private String calculateHeaderDate(long timeMs) { + int titleIndex = + (int) + ((Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()) - timeMs) + / ONE_DAY_MS); + String headerDate; + if (titleIndex < mTitles.size()) { + headerDate = mTitles.get(titleIndex); + } else { + headerDate = + DateUtils.formatDateTime( + getContext(), + timeMs, + DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH); + } + return headerDate; + } + + private List<ScheduledRecording> recordedProgramsToScheduledRecordings( + List<RecordedProgram> programs, int maxDays) { + List<ScheduledRecording> result = new ArrayList<>(); + for (RecordedProgram recordedProgram : programs) { + ScheduledRecording scheduledRecording = + recordedProgramsToScheduledRecordings(recordedProgram, maxDays); + if (scheduledRecording != null) { + result.add(scheduledRecording); + } + } + return result; + } + + @Nullable + private ScheduledRecording recordedProgramsToScheduledRecordings( + RecordedProgram program, int maxDays) { + long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()); + if (maxDays + < Utils.computeDateDifference( + program.getStartTimeUtcMillis(), + firstMillisecondToday)) { + return null; + } + ScheduledRecording scheduledRecording = ScheduledRecording.builder(program).build(); + mRecordedProgramScheduleMap.put(program.getId(), scheduledRecording); + return scheduledRecording; + } + + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + } + if (findRowByScheduledRecording(schedule) == null + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) { + addScheduleRow(schedule); + } + } + + public void onScheduledRecordingAdded(RecordedProgram program) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingAdded: " + program); + } + if (mRecordedProgramScheduleMap.get(program.getId()) != null) { + return; + } + ScheduledRecording schedule = + recordedProgramsToScheduledRecordings(program, MAX_HISTORY_DAYS); + if (schedule == null) { + return; + } + addScheduleRow(schedule); + } + + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + } + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + } + } + + public void onScheduledRecordingRemoved(RecordedProgram program) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingRemoved: " + program); + } + ScheduledRecording scheduledRecording = mRecordedProgramScheduleMap.get(program.getId()); + if (scheduledRecording != null) { + mRecordedProgramScheduleMap.remove(program.getId()); + ScheduleRow row = findRowByRecordedProgram(program); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + } + } + } + + public void onScheduledRecordingUpdated(ScheduledRecording schedule) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + } + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + row.setSchedule(schedule); + if (schedule.getState() != ScheduledRecording.STATE_RECORDING_FAILED) { + // Only handle failed schedules. Finished schedules are handled as recorded programs + removeScheduleRow(row); + } + notifyArrayItemRangeChanged(indexOf(row), 1); + } + } + + public void onScheduledRecordingUpdated(RecordedProgram program) { + if (DEBUG) { + Log.d(TAG, "onScheduledRecordingUpdated: " + program); + } + ScheduleRow row = findRowByRecordedProgram(program); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + ScheduledRecording schedule = mRecordedProgramScheduleMap.get(program.getId()); + if (schedule != null) { + mRecordedProgramScheduleMap.remove(program.getId()); + } + } + onScheduledRecordingAdded(program); + } + + private void addScheduleRow(ScheduledRecording recording) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class)); + if (recording != null) { + int pre = -1; + int index = 0; + for (; index < size(); index++) { + if (get(index) instanceof ScheduleRow) { + ScheduleRow scheduleRow = (ScheduleRow) get(index); + if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed() + .compare(scheduleRow.getSchedule(), recording) > 0) { + break; + } + pre = index; + } + } + long deadLine = Utils.getFirstMillisecondOfDay(recording.getStartTimeMs()); + if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) { + SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow(); + headerRow.setItemCount(headerRow.getItemCount() + 1); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(++pre, addedRow); + updateHeaderDescription(headerRow); + } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { + SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); + headerRow.setItemCount(headerRow.getItemCount() + 1); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(index, addedRow); + updateHeaderDescription(headerRow); + } else { + SchedulesHeaderRow headerRow = + new DateHeaderRow( + calculateHeaderDate(deadLine), + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, 1, 1), + 1, + deadLine); + add(++pre, headerRow); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(pre, addedRow); + } + } + } + + private DateHeaderRow getHeaderRow(int index) { + return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); + } + + /** Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. */ + private ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) { + if (recording == null) { + return null; + } + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { + if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { + return (ScheduleRow) item; + } + } + } + return null; + } + + private ScheduleRow findRowByRecordedProgram(RecordedProgram program) { + if (program == null) { + return null; + } + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.hasRecordedProgram() + && row.getSchedule().getRecordedProgramId() == program.getId()) { + return (ScheduleRow) item; + } + } + } + return null; + } + + private void removeScheduleRow(ScheduleRow scheduleRow) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class)); + if (scheduleRow != null) { + scheduleRow.setSchedule(null); + SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); + remove(scheduleRow); + // Changes the count information of header which the removed row belongs to. + if (headerRow != null) { + int currentCount = headerRow.getItemCount(); + headerRow.setItemCount(--currentCount); + if (headerRow.getItemCount() == 0) { + remove(headerRow); + } else { + replace(indexOf(headerRow), headerRow); + updateHeaderDescription(headerRow); + } + } + } + } + + private void updateHeaderDescription(SchedulesHeaderRow headerRow) { + headerRow.setDescription( + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + headerRow.getItemCount(), + headerRow.getItemCount())); + } +} diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java index a0410bb3..82b85630 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java @@ -20,23 +20,19 @@ import android.app.Activity; import android.app.ProgressDialog; import android.os.Bundle; import android.support.annotation.IntDef; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; import com.android.tv.data.Program; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.EpisodicProgramLoadTask; import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.dvr.ui.BigArguments; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; -/** - * Activity to show the list of recording schedules. - */ +/** Activity to show the list of recording schedules. */ public class DvrSchedulesActivity extends Activity { /** * The key for the type of the schedules which will be listed in the list. The type of the value @@ -47,9 +43,7 @@ public class DvrSchedulesActivity extends Activity { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE}) public @interface ScheduleListType {} - /** - * A type which means the activity will display the full scheduled recordings. - */ + /** A type which means the activity will display the full scheduled recordings. */ public static final int TYPE_FULL_SCHEDULE = 0; /** * A type which means the activity will display a scheduled recording list of a series @@ -59,7 +53,7 @@ public class DvrSchedulesActivity extends Activity { @Override public void onCreate(final Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); // Pass null to prevent automatically re-creating fragments super.onCreate(null); setContentView(R.layout.activity_dvr_schedules); @@ -67,20 +61,29 @@ public class DvrSchedulesActivity extends Activity { if (scheduleType == TYPE_FULL_SCHEDULE) { DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); schedulesFragment.setArguments(getIntent().getExtras()); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); + getFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, schedulesFragment) + .commit(); } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - if (BigArguments.getArgument(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) { + if (BigArguments.getArgument( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) + != null) { // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need // to reset the BigArguments. showDvrSeriesSchedulesFragment(getIntent().getExtras()); } else { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + final ProgressDialog dialog = + ProgressDialog.show( + this, + null, + getString(R.string.dvr_series_progress_message_reading_programs)); + SeriesRecording seriesRecording = + getIntent() + .getExtras() + .getParcelable( + DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); // To get programs faster, hold the update of the series schedules. SeriesRecordingScheduler.getInstance(this).pauseUpdate(); new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { @@ -110,7 +113,9 @@ public class DvrSchedulesActivity extends Activity { private void showDvrSeriesSchedulesFragment(Bundle args) { DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); + getFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, schedulesFragment) + .commit(); } } diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java index c906c62a..c86721e8 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java @@ -23,12 +23,9 @@ import android.graphics.RectF; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; - import com.android.tv.R; -/** - * A view used for focus in schedules list. - */ +/** A view used for focus in schedules list. */ public class DvrSchedulesFocusView extends View { private final Paint mPaint; private final RectF mRoundRectF = new RectF(); @@ -76,13 +73,11 @@ public class DvrSchedulesFocusView extends View { private int getRoundRectRadius() { if (TextUtils.equals(mViewTag, mHeaderFocusViewTag)) { - return getResources().getDimensionPixelSize( - R.dimen.dvr_schedules_header_selector_radius); + return getResources() + .getDimensionPixelSize(R.dimen.dvr_schedules_header_selector_radius); } else if (TextUtils.equals(mViewTag, mItemFocusViewTag)) { return getResources().getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius); } return 0; } } - - diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index 3cbb500a..d97b61f4 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,14 +18,11 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; - import com.android.tv.R; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; -/** - * A fragment to show the list of schedule recordings. - */ +/** A fragment to show the list of schedule recordings. */ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment { @Override public void onCreate(Bundle savedInstanceState) { @@ -46,8 +43,8 @@ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment { } @Override - public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor) { - return new ScheduleRowAdapter(getContext(), presenterSelecor); + public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelector) { + return new ScheduleRowAdapter(getContext(), presenterSelector); } @Override @@ -73,11 +70,11 @@ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment { if (args != null) { recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING); } - final int selectedPostion = getRowsAdapter().indexOf( - getRowsAdapter().findRowByScheduledRecording(recording)); + final int selectedPostion = + getRowsAdapter().indexOf(getRowsAdapter().findRowByScheduledRecording(recording)); if (selectedPostion != -1) { return selectedPostion; } return super.getFirstItemPosition(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index 57e7a88f..d376e358 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -1,18 +1,18 @@ /* -* 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 -*/ + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ package com.android.tv.dvr.ui.list; @@ -30,10 +30,8 @@ import android.transition.Fade; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; @@ -41,25 +39,21 @@ import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.EpisodicProgramLoadTask; import com.android.tv.dvr.ui.BigArguments; - import java.util.Collections; import java.util.List; -/** - * A fragment to show the list of series schedule recordings. - */ +/** A fragment to show the list of series schedule recordings. */ @TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { /** - * The key for series recording whose scheduled recording list will be displayed. - * Type: {@link SeriesRecording} + * The key for series recording whose scheduled recording list will be displayed. Type: {@link + * SeriesRecording} */ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; /** - * The key for programs which belong to the series recording whose scheduled recording list - * will be displayed. - * Type: List<{@link Program}> + * The key for programs which belong to the series recording whose scheduled recording list will + * be displayed. Type: List<{@link Program}> */ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = "series_schedules_key_series_programs"; @@ -73,7 +67,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {} @Override public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { @@ -101,26 +95,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { }; private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final ContentObserver mContentObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - executeProgramLoadingTask(); - } - }; + private final ContentObserver mContentObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; - private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { } + private final ChannelDataManager.Listener mChannelListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() {} - @Override - public void onChannelListUpdated() { - executeProgramLoadingTask(); - } + @Override + public void onChannelListUpdated() { + executeProgramLoadingTask(); + } - @Override - public void onChannelBrowsableChanged() { } - }; + @Override + public void onChannelBrowsableChanged() {} + }; public DvrSeriesSchedulesFragment() { setEnterTransition(new Fade(Fade.IN)); @@ -132,8 +128,8 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { Bundle args = getArguments(); if (args != null) { mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); - mPrograms = (List<Program>) BigArguments.getArgument( - SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + mPrograms = + (List<Program>) BigArguments.getArgument(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); BigArguments.reset(); } if (args == null || mPrograms == null) { @@ -144,18 +140,19 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); mChannelDataManager = singletons.getChannelDataManager(); mChannelDataManager.addListener(mChannelListener); mDvrDataManager = singletons.getDvrDataManager(); mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener); - getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, - mContentObserver); + getContext() + .getContentResolver() + .registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { onProgramsUpdated(); return super.onCreateView(inflater, container, savedInstanceState); } @@ -218,14 +215,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { if (mProgramLoadTask != null) { mProgramLoadTask.cancel(true); } - mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { - @Override - protected void onPostExecute(List<Program> programs) { - mPrograms = programs == null ? Collections.EMPTY_LIST : programs; - onProgramsUpdated(); - } - }; - mProgramLoadTask.setLoadCurrentProgram(true) + mProgramLoadTask = + new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { + @Override + protected void onPostExecute(List<Program> programs) { + mPrograms = programs == null ? Collections.EMPTY_LIST : programs; + onProgramsUpdated(); + } + }; + mProgramLoadTask + .setLoadCurrentProgram(true) .setLoadDisallowedProgram(true) .setLoadScheduledEpisode(true) .setIgnoreChannelOption(true) diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java index 2af832ec..d5808412 100644 --- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -17,29 +17,27 @@ package com.android.tv.dvr.ui.list; import android.content.Context; - import com.android.tv.data.Program; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.ScheduledRecording.Builder; import com.android.tv.dvr.ui.DvrUiHelper; -/** - * A class for the episodic program. - */ +/** A class for the episodic program. */ class EpisodicProgramRow extends ScheduleRow { private final String mInputId; private final Program mProgram; - public EpisodicProgramRow(String inputId, Program program, ScheduledRecording recording, + public EpisodicProgramRow( + String inputId, + Program program, + ScheduledRecording recording, SchedulesHeaderRow headerRow) { super(recording, headerRow); mInputId = inputId; mProgram = program; } - /** - * Returns the program. - */ + /** Returns the program. */ public Program getProgram() { return mProgram; } @@ -82,9 +80,6 @@ class EpisodicProgramRow extends ScheduleRow { @Override public String toString() { - return super.toString() - + "(inputId=" + mInputId - + ",program=" + mProgram - + ")"; + return super.toString() + "(inputId=" + mInputId + ",program=" + mProgram + ")"; } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 91ba393a..b739c18f 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -18,14 +18,11 @@ package com.android.tv.dvr.ui.list; import android.content.Context; import android.support.annotation.Nullable; - import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrUiHelper; -/** - * A class for schedule recording row. - */ +/** A class for schedule recording row. */ class ScheduleRow { private final SchedulesHeaderRow mHeaderRow; @Nullable private ScheduledRecording mSchedule; @@ -37,113 +34,89 @@ class ScheduleRow { mHeaderRow = headerRow; } - /** - * Gets which {@link SchedulesHeaderRow} this schedule row belongs to. - */ + /** Gets which {@link SchedulesHeaderRow} this schedule row belongs to. */ public SchedulesHeaderRow getHeaderRow() { return mHeaderRow; } - /** - * Returns the recording schedule. - */ + /** Returns the recording schedule. */ @Nullable public ScheduledRecording getSchedule() { return mSchedule; } - /** - * Checks if the stop recording has been requested or not. - */ + /** Checks if the stop recording has been requested or not. */ public boolean isStopRecordingRequested() { return mStopRecordingRequested; } - /** - * Sets the flag of stop recording request. - */ + /** Sets the flag of stop recording request. */ public void setStopRecordingRequested(boolean stopRecordingRequested) { SoftPreconditions.checkState(!mStartRecordingRequested); mStopRecordingRequested = stopRecordingRequested; } - /** - * Checks if the start recording has been requested or not. - */ + /** Checks if the start recording has been requested or not. */ public boolean isStartRecordingRequested() { return mStartRecordingRequested; } - /** - * Sets the flag of start recording request. - */ + /** Sets the flag of start recording request. */ public void setStartRecordingRequested(boolean startRecordingRequested) { SoftPreconditions.checkState(!mStopRecordingRequested); mStartRecordingRequested = startRecordingRequested; } - /** - * Sets the recording schedule. - */ + /** Sets the recording schedule. */ public void setSchedule(@Nullable ScheduledRecording schedule) { mSchedule = schedule; } - /** - * Returns the channel ID. - */ + /** Returns the channel ID. */ public long getChannelId() { return mSchedule != null ? mSchedule.getChannelId() : -1; } - /** - * Returns the start time. - */ + /** Returns the start time. */ public long getStartTimeMs() { return mSchedule != null ? mSchedule.getStartTimeMs() : -1; } - /** - * Returns the end time. - */ + /** Returns the end time. */ public long getEndTimeMs() { return mSchedule != null ? mSchedule.getEndTimeMs() : -1; } - /** - * Returns the duration. - */ + /** Returns the duration. */ public final long getDuration() { return getEndTimeMs() - getStartTimeMs(); } - /** - * Checks if the program is on air. - */ + /** Checks if the program is on air. */ public final boolean isOnAir() { long currentTimeMs = System.currentTimeMillis(); return getStartTimeMs() <= currentTimeMs && getEndTimeMs() > currentTimeMs; } - /** - * Checks if the schedule is not started. - */ + /** Checks if the schedule is not started. */ public final boolean isRecordingNotStarted() { return mSchedule != null && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; } - /** - * Checks if the schedule is in progress. - */ + /** Checks if the schedule is in progress. */ public final boolean isRecordingInProgress() { return mSchedule != null && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS; } - /** - * Checks if the schedule has been canceled or not. - */ + /** Checks if the schedule is failed. */ + public final boolean isRecordingFailed() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED; + } + + /** Checks if the schedule has been canceled or not. */ public final boolean isScheduleCanceled() { return mSchedule != null && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED; @@ -152,28 +125,29 @@ class ScheduleRow { public boolean isRecordingFinished() { return mSchedule != null && (mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED - || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED - || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED); + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED); } - /** - * Creates and returns the new schedule with the existing information. - */ + public boolean hasRecordedProgram() { + return mSchedule != null + && mSchedule.getRecordedProgramId() != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED; + } + + /** Creates and returns the new schedule with the existing information. */ public ScheduledRecording.Builder createNewScheduleBuilder() { return mSchedule != null ? ScheduledRecording.buildFrom(mSchedule) : null; } - /** - * Returns the program title with episode number. - */ + /** Returns the program title with episode number. */ public String getProgramTitleWithEpisodeNumber(Context context) { - return mSchedule != null ? DvrUiHelper.getStyledTitleWithEpisodeNumber(context, - mSchedule, 0).toString() : null; + return mSchedule != null + ? DvrUiHelper.getStyledTitleWithEpisodeNumber(context, mSchedule, 0).toString() + : null; } - /** - * Returns the program title including the season/episode number. - */ + /** Returns the program title including the season/episode number. */ public String getEpisodeDisplayTitle(Context context) { return mSchedule != null ? mSchedule.getEpisodeDisplayTitle(context) : null; } @@ -181,15 +155,16 @@ class ScheduleRow { @Override public String toString() { return getClass().getSimpleName() - + "(schedule=" + mSchedule - + ",stopRecordingRequested=" + mStopRecordingRequested - + ",startRecordingRequested=" + mStartRecordingRequested + + "(schedule=" + + mSchedule + + ",stopRecordingRequested=" + + mStopRecordingRequested + + ",startRecordingRequested=" + + mStartRecordingRequested + ")"; } - /** - * Checks if the {@code schedule} is for the program or channel. - */ + /** Checks if the {@code schedule} is for the program or channel. */ public boolean matchSchedule(ScheduledRecording schedule) { if (mSchedule == null) { return false; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 97d60473..ef4a4337 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -16,7 +16,9 @@ package com.android.tv.dvr.ui.list; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -27,11 +29,11 @@ import android.util.ArraySet; import android.util.Log; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -40,14 +42,14 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -/** - * An adapter for {@link ScheduleRow}. - */ +/** An adapter for {@link ScheduleRow}. */ +@TargetApi(VERSION_CODES.N) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated class ScheduleRowAdapter extends ArrayObjectAdapter { private static final String TAG = "ScheduleRowAdapter"; private static final boolean DEBUG = false; - private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); private static final int MSG_UPDATE_ROW = 1; @@ -55,16 +57,17 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { private final List<String> mTitles = new ArrayList<>(); private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>(); - private final Handler mHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_ROW) { - long currentTimeMs = System.currentTimeMillis(); - handleUpdateRow(currentTimeMs); - sendNextUpdateMessage(currentTimeMs); - } - } - }; + private final Handler mHandler = + new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_ROW) { + long currentTimeMs = System.currentTimeMillis(); + handleUpdateRow(currentTimeMs); + sendNextUpdateMessage(currentTimeMs); + } + } + }; public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) { super(classPresenterSelector); @@ -73,37 +76,41 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { mTitles.add(mContext.getString(R.string.dvr_date_tomorrow)); } - /** - * Returns context. - */ + /** Returns context. */ protected Context getContext() { return mContext; } - /** - * Starts schedule row adapter. - */ + /** Starts schedule row adapter. */ public void start() { clear(); - List<ScheduledRecording> recordingList = TvApplication.getSingletons(mContext) - .getDvrDataManager().getNonStartedScheduledRecordings(); - recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager() - .getStartedRecordings()); - Collections.sort(recordingList, - ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + List<ScheduledRecording> recordingList = + TvSingletons.getSingletons(mContext) + .getDvrDataManager() + .getNonStartedScheduledRecordings(); + recordingList.addAll( + TvSingletons.getSingletons(mContext).getDvrDataManager().getStartedRecordings()); + Collections.sort( + recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis()); - for (int i = 0; i < recordingList.size();) { + for (int i = 0; i < recordingList.size(); ) { ArrayList<ScheduledRecording> section = new ArrayList<>(); while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() < deadLine) { section.add(recordingList.get(i++)); } if (!section.isEmpty()) { - SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), - mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_section_subtitle, section.size(), section.size()), - section.size(), deadLine); + SchedulesHeaderRow headerRow = + new DateHeaderRow( + calculateHeaderDate(deadLine), + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + section.size(), + section.size()), + section.size(), + deadLine); add(headerRow); - for(ScheduledRecording recording : section){ + for (ScheduledRecording recording : section) { add(new ScheduleRow(recording, headerRow)); } } @@ -113,25 +120,29 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } private String calculateHeaderDate(long deadLine) { - int titleIndex = (int) ((deadLine - - Utils.getLastMillisecondOfDay(System.currentTimeMillis())) / ONE_DAY_MS); + int titleIndex = + (int) + ((deadLine - Utils.getLastMillisecondOfDay(System.currentTimeMillis())) + / ONE_DAY_MS); String headerDate; if (titleIndex < mTitles.size()) { headerDate = mTitles.get(titleIndex); } else { - headerDate = DateUtils.formatDateTime(getContext(), deadLine, - DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_ABBREV_MONTH); + headerDate = + DateUtils.formatDateTime( + getContext(), + deadLine, + DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH); } return headerDate; } - /** - * Stops schedules row adapter. - */ + /** Stops schedules row adapter. */ public void stop() { mHandler.removeCallbacksAndMessages(null); - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); for (int i = 0; i < size(); i++) { if (get(i) instanceof ScheduleRow) { ScheduleRow row = (ScheduleRow) get(i); @@ -142,9 +153,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } } - /** - * Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. - */ + /** Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. */ public ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) { if (recording == null) { return null; @@ -167,7 +176,8 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { continue; } ScheduleRow row = (ScheduleRow) item; - if (row.getSchedule() != null && row.isStartRecordingRequested() + if (row.getSchedule() != null + && row.isStartRecordingRequested() && row.matchSchedule(schedule)) { return row; } @@ -185,7 +195,8 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { if (get(index) instanceof ScheduleRow) { ScheduleRow scheduleRow = (ScheduleRow) get(index); if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare( - scheduleRow.getSchedule(), recording) > 0) { + scheduleRow.getSchedule(), recording) + > 0) { break; } pre = index; @@ -205,9 +216,14 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { add(index, addedRow); updateHeaderDescription(headerRow); } else { - SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), - mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_section_subtitle, 1, 1), 1, deadLine); + SchedulesHeaderRow headerRow = + new DateHeaderRow( + calculateHeaderDate(deadLine), + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, 1, 1), + 1, + deadLine); add(++pre, headerRow); ScheduleRow addedRow = new ScheduleRow(recording, headerRow); add(pre, addedRow); @@ -241,14 +257,15 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } private void updateHeaderDescription(SchedulesHeaderRow headerRow) { - headerRow.setDescription(mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_section_subtitle, - headerRow.getItemCount(), headerRow.getItemCount())); + headerRow.setDescription( + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + headerRow.getItemCount(), + headerRow.getItemCount())); } - /** - * Called when a schedule recording is added to dvr date manager. - */ + /** Called when a schedule recording is added to dvr date manager. */ public void onScheduledRecordingAdded(ScheduledRecording schedule) { if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); ScheduleRow row = findRowWithStartRequest(schedule); @@ -263,9 +280,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } } - /** - * Called when a schedule recording is removed from dvr date manager. - */ + /** Called when a schedule recording is removed from dvr date manager. */ public void onScheduledRecordingRemoved(ScheduledRecording schedule) { if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); ScheduleRow row = findRowByScheduledRecording(schedule); @@ -276,9 +291,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } } - /** - * Called when a schedule recording is updated in dvr date manager. - */ + /** Called when a schedule recording is updated in dvr date manager. */ public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); ScheduleRow row = findRowByScheduledRecording(schedule); @@ -329,9 +342,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } } - /** - * Checks if there is a row which requested start/stop recording. - */ + /** Checks if there is a row which requested start/stop recording. */ protected boolean isStartOrStopRequested() { for (int i = 0; i < size(); i++) { Object item = get(i); @@ -345,16 +356,12 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { return false; } - /** - * Delays update of the row. - */ + /** Delays update of the row. */ protected void addPendingUpdate(ScheduleRow row) { mPendingUpdate.add(row); } - /** - * Executes the pending updates. - */ + /** Executes the pending updates. */ protected void executePendingUpdate() { for (ScheduleRow row : mPendingUpdate) { int index = indexOf(row); @@ -365,21 +372,17 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { mPendingUpdate.clear(); } - /** - * To check whether the recording should be kept or not. - */ + /** To check whether the recording should be kept or not. */ protected boolean willBeKept(ScheduledRecording schedule) { // CANCELED state means that the schedule was removed temporarily, which should be shown // in the list so that the user can reschedule it. return schedule.getEndTimeMs() > System.currentTimeMillis() && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); + || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); } - /** - * Handle the message to update/remove rows. - */ + /** Handle the message to update/remove rows. */ protected void handleUpdateRow(long currentTimeMs) { for (int i = 0; i < size(); i++) { Object item = get(i); @@ -392,9 +395,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { } } - /** - * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. - */ + /** Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. */ protected long getNextTimerMs(long currentTimeMs) { long earliest = Long.MAX_VALUE; for (int i = 0; i < size(); i++) { @@ -411,15 +412,12 @@ class ScheduleRowAdapter extends ArrayObjectAdapter { return earliest; } - /** - * Send update message at the time returned by {@link #getNextTimerMs}. - */ + /** Send update message at the time returned by {@link #getNextTimerMs}. */ protected final void sendNextUpdateMessage(long currentTimeMs) { mHandler.removeMessages(MSG_UPDATE_ROW); long nextTime = getNextTimerMs(currentTimeMs); if (nextTime != Long.MAX_VALUE) { - mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, - nextTime - System.currentTimeMillis()); + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, nextTime - System.currentTimeMillis()); } } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index dc4e3c41..38d3d582 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -18,7 +18,6 @@ package com.android.tv.dvr.ui.list; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; @@ -37,11 +36,11 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; @@ -50,21 +49,22 @@ import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -/** - * A RowPresenter for {@link ScheduleRow}. - */ +/** A RowPresenter for {@link ScheduleRow}. */ @TargetApi(Build.VERSION_CODES.N) class ScheduleRowPresenter extends RowPresenter { private static final String TAG = "ScheduleRowPresenter"; @Retention(RetentionPolicy.SOURCE) - @IntDef({ACTION_START_RECORDING, ACTION_STOP_RECORDING, ACTION_CREATE_SCHEDULE, - ACTION_REMOVE_SCHEDULE}) + @IntDef({ + ACTION_START_RECORDING, + ACTION_STOP_RECORDING, + ACTION_CREATE_SCHEDULE, + ACTION_REMOVE_SCHEDULE + }) public @interface ScheduleRowAction {} /** An action to start recording. */ public static final int ACTION_START_RECORDING = 1; @@ -85,9 +85,7 @@ class ScheduleRowPresenter extends RowPresenter { private int mLastFocusedViewId; - /** - * A ViewHolder for {@link ScheduleRow} - */ + /** A ViewHolder for {@link ScheduleRow} */ public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder { private ScheduleRowPresenter mPresenter; @ScheduleRowAction private int[] mActions; @@ -118,29 +116,30 @@ class ScheduleRowPresenter extends RowPresenter { new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - if (view.isFocused()) { - mPresenter.mLastFocusedViewId = view.getId(); - } - updateSelector(); - } - }); + view.post( + new Runnable() { + @Override + public void run() { + if (view.isFocused()) { + mPresenter.mLastFocusedViewId = view.getId(); + } + updateSelector(); + } + }); } }; public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { super(view); mPresenter = presenter; - mLtr = view.getContext().getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; + mLtr = + view.getContext().getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container); - mSecondActionContainer = (RelativeLayout) view.findViewById( - R.id.action_second_container); + mSecondActionContainer = + (RelativeLayout) view.findViewById(R.id.action_second_container); mSecondActionView = (ImageView) view.findViewById(R.id.action_second); - mFirstActionContainer = (RelativeLayout) view.findViewById( - R.id.action_first_container); + mFirstActionContainer = (RelativeLayout) view.findViewById(R.id.action_first_container); mFirstActionView = (ImageView) view.findViewById(R.id.action_first); mSelectorView = view.findViewById(R.id.selector); mTimeView = (TextView) view.findViewById(R.id.time); @@ -151,47 +150,48 @@ class ScheduleRowPresenter extends RowPresenter { Resources res = view.getResources(); mSelectorTranslationDelta = res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) - - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_translation_delta); - mSelectorWidthDelta = res.getDimensionPixelSize( - R.dimen.dvr_schedules_item_focus_width_delta); + - res.getDimensionPixelSize( + R.dimen.dvr_schedules_item_focus_translation_delta); + mSelectorWidthDelta = + res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_width_delta); mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius); - int fullWidth = res.getDimensionPixelSize( - R.dimen.dvr_schedules_item_width) - - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding); + int fullWidth = + res.getDimensionPixelSize(R.dimen.dvr_schedules_item_width) + - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding); mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius; - mInfoContainerTargetWidthWithOneAction = fullWidth - - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) - - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width) - + mRoundRectRadius + mSelectorWidthDelta; - mInfoContainerTargetWidthWithTwoAction = mInfoContainerTargetWidthWithOneAction - - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) - - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size); + mInfoContainerTargetWidthWithOneAction = + fullWidth + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width) + + mRoundRectRadius + + mSelectorWidthDelta; + mInfoContainerTargetWidthWithTwoAction = + mInfoContainerTargetWidthWithOneAction + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size); mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener); mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); } - /** - * Returns time view. - */ + /** Returns time view. */ public TextView getTimeView() { return mTimeView; } - /** - * Returns title view. - */ + /** Returns title view. */ public TextView getProgramTitleView() { return mProgramTitleView; } private void updateSelector() { - int animationDuration = mSelectorView.getResources().getInteger( - android.R.integer.config_shortAnimTime); + int animationDuration = + mSelectorView.getResources().getInteger(android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); - if (mInfoContainer.isFocused() || mSecondActionContainer.isFocused() + if (mInfoContainer.isFocused() + || mSecondActionContainer.isFocused() || mFirstActionContainer.isFocused()) { final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams(); final int targetWidth; @@ -208,33 +208,50 @@ class ScheduleRowPresenter extends RowPresenter { } else if (mSecondActionContainer.isFocused()) { targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius); } else { - targetWidth = mFirstActionContainer.getWidth() + mRoundRectRadius - + mSelectorTranslationDelta; + targetWidth = + mFirstActionContainer.getWidth() + + mRoundRectRadius + + mSelectorTranslationDelta; } float targetTranslationX; if (mInfoContainer.isFocused()) { - targetTranslationX = mLtr ? mInfoContainer.getLeft() - mRoundRectRadius - - mSelectorView.getLeft() : - mInfoContainer.getRight() + mRoundRectRadius - mSelectorView.getRight(); + targetTranslationX = + mLtr + ? mInfoContainer.getLeft() + - mRoundRectRadius + - mSelectorView.getLeft() + : mInfoContainer.getRight() + + mRoundRectRadius + - mSelectorView.getRight(); } else if (mSecondActionContainer.isFocused()) { if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) { - targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - - mSelectorView.getLeft() - : mSecondActionContainer.getRight() - mSelectorView.getRight(); + targetTranslationX = + mLtr + ? mSecondActionContainer.getLeft() - mSelectorView.getLeft() + : mSecondActionContainer.getRight() + - mSelectorView.getRight(); } else { - targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - - (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - - mSelectorView.getLeft() - : mSecondActionContainer.getRight() + - (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - - mSelectorView.getRight(); + targetTranslationX = + mLtr + ? mSecondActionContainer.getLeft() + - (mRoundRectRadius + - mSecondActionContainer.getWidth() / 2) + - mSelectorView.getLeft() + : mSecondActionContainer.getRight() + + (mRoundRectRadius + - mSecondActionContainer.getWidth() / 2) + - mSelectorView.getRight(); } } else { - targetTranslationX = mLtr ? mFirstActionContainer.getLeft() - - mSelectorTranslationDelta - mSelectorView.getLeft() - : mFirstActionContainer.getRight() + mSelectorTranslationDelta - - mSelectorView.getRight(); + targetTranslationX = + mLtr + ? mFirstActionContainer.getLeft() + - mSelectorTranslationDelta + - mSelectorView.getLeft() + : mFirstActionContainer.getRight() + + mSelectorTranslationDelta + - mSelectorView.getRight(); } if (mSelectorView.getAlpha() == 0) { @@ -246,57 +263,72 @@ class ScheduleRowPresenter extends RowPresenter { // animate the selector in and to the proper width and translation X. final float deltaWidth = lp.width - targetWidth; mSelectorView.animate().cancel(); - mSelectorView.animate().translationX(targetTranslationX).alpha(1f) - .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - // Set width to the proper width for this animation step. - lp.width = targetWidth + Math.round( - deltaWidth * (1f - animation.getAnimatedFraction())); - mSelectorView.requestLayout(); - } - }).setDuration(animationDuration).setInterpolator(interpolator).start(); + mSelectorView + .animate() + .translationX(targetTranslationX) + .alpha(1f) + .setUpdateListener( + animation -> { + // Set width to the proper width for this animation step. + float fraction = 1f - animation.getAnimatedFraction(); + lp.width = targetWidth + Math.round(deltaWidth * fraction); + mSelectorView.requestLayout(); + }) + .setDuration(animationDuration) + .setInterpolator(interpolator) + .start(); if (mPendingAnimationRunnable != null) { mPendingAnimationRunnable.run(); mPendingAnimationRunnable = null; } } else { mSelectorView.animate().cancel(); - mSelectorView.animate().alpha(0f).setDuration(animationDuration) - .setInterpolator(interpolator).setUpdateListener(null).start(); + mSelectorView + .animate() + .alpha(0f) + .setDuration(animationDuration) + .setInterpolator(interpolator) + .setUpdateListener(null) + .start(); } } - /** - * Grey out the information body. - */ + /** Grey out the information body. */ public void greyOutInfo() { - mTimeView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info_grey, null)); - mProgramTitleView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info_grey, null)); - mInfoSeparatorView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info_grey, null)); - mChannelNameView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info_grey, null)); - mConflictInfoView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info_grey, null)); + mTimeView.setTextColor( + mInfoContainer + .getResources() + .getColor(R.color.dvr_schedules_item_info_grey, null)); + mProgramTitleView.setTextColor( + mInfoContainer + .getResources() + .getColor(R.color.dvr_schedules_item_info_grey, null)); + mInfoSeparatorView.setTextColor( + mInfoContainer + .getResources() + .getColor(R.color.dvr_schedules_item_info_grey, null)); + mChannelNameView.setTextColor( + mInfoContainer + .getResources() + .getColor(R.color.dvr_schedules_item_info_grey, null)); + mConflictInfoView.setTextColor( + mInfoContainer + .getResources() + .getColor(R.color.dvr_schedules_item_info_grey, null)); } - /** - * Reverse grey out operation. - */ + /** Reverse grey out operation. */ public void whiteBackInfo() { - mTimeView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info, null)); - mProgramTitleView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_main, null)); - mInfoSeparatorView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info, null)); - mChannelNameView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info, null)); - mConflictInfoView.setTextColor(mInfoContainer.getResources().getColor(R.color - .dvr_schedules_item_info, null)); + mTimeView.setTextColor( + mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); + mProgramTitleView.setTextColor( + mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_main, null)); + mInfoSeparatorView.setTextColor( + mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); + mChannelNameView.setTextColor( + mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); + mConflictInfoView.setTextColor( + mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); } } @@ -304,32 +336,29 @@ class ScheduleRowPresenter extends RowPresenter { setHeaderPresenter(null); setSelectEffectEnabled(false); mContext = context; - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); - mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); - mTunerConflictWillNotBeRecordedInfo = mContext.getString( - R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info); - mTunerConflictWillBePartiallyRecordedInfo = mContext.getString( - R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded); - mAnimationDuration = mContext.getResources().getInteger( - android.R.integer.config_shortAnimTime); + mDvrManager = TvSingletons.getSingletons(context).getDvrManager(); + mDvrScheduleManager = TvSingletons.getSingletons(context).getDvrScheduleManager(); + mTunerConflictWillNotBeRecordedInfo = + mContext.getString(R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info); + mTunerConflictWillBePartiallyRecordedInfo = + mContext.getString( + R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded); + mAnimationDuration = + mContext.getResources().getInteger(android.R.integer.config_shortAnimTime); } @Override public ViewHolder createRowViewHolder(ViewGroup parent) { - return onGetScheduleRowViewHolder(LayoutInflater.from(mContext) - .inflate(R.layout.dvr_schedules_item, parent, false)); + return onGetScheduleRowViewHolder( + LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item, parent, false)); } - /** - * Returns context. - */ + /** Returns context. */ protected Context getContext() { return mContext; } - /** - * Returns DVR manager. - */ + /** Returns DVR manager. */ protected DvrManager getDvrManager() { return mDvrManager; } @@ -341,54 +370,77 @@ class ScheduleRowPresenter extends RowPresenter { ScheduleRow row = (ScheduleRow) item; @ScheduleRowAction int[] actions = getAvailableActions(row); viewHolder.mActions = actions; - viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (isInfoClickable(row)) { - onInfoClicked(row); - } - } - }); + viewHolder.mInfoContainer.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (isInfoClickable(row)) { + onInfoClicked(row); + } + } + }); - viewHolder.mFirstActionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onActionClicked(actions[0], row); - } - }); + viewHolder.mFirstActionContainer.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + onActionClicked(actions[0], row); + } + }); - viewHolder.mSecondActionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - onActionClicked(actions[1], row); - } - }); + viewHolder.mSecondActionContainer.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + onActionClicked(actions[1], row); + } + }); viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); - programInfoText = mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); + programInfoText = + mContext.getResources() + .getQuantityString( + R.plurals.dvr_schedules_recording_duration, + durationMins, + durationMins); } String channelName = getChannelNameText(row); viewHolder.mProgramTitleView.setText(programInfoText); - viewHolder.mInfoSeparatorView.setVisibility((!TextUtils.isEmpty(programInfoText) - && !TextUtils.isEmpty(channelName)) ? View.VISIBLE : View.GONE); + viewHolder.mInfoSeparatorView.setVisibility( + (!TextUtils.isEmpty(programInfoText) && !TextUtils.isEmpty(channelName)) + ? View.VISIBLE + : View.GONE); viewHolder.mChannelNameView.setText(channelName); if (actions != null) { switch (actions.length) { case 2: viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1])); - // pass through + // fall through case 1: viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0])); break; + default: // fall out } } - if (mDvrManager.isConflicting(row.getSchedule())) { + ScheduledRecording schedule = row.getSchedule(); + if (mDvrManager.isConflicting(schedule) + || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) + && schedule != null + && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) { String conflictInfo; - if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { + if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) + && schedule != null + && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + // TODO(b/72638385): show real error messages + // TODO(b/72638385): use a better name for ConflictInfoXXX + conflictInfo = "Failed"; + if (schedule.getFailedReason() != null) { + conflictInfo += " (Error code: " + schedule.getFailedReason() + ")"; + } + } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; } else { conflictInfo = mTunerConflictWillNotBeRecordedInfo; @@ -422,51 +474,48 @@ class ScheduleRowPresenter extends RowPresenter { } } - /** - * Returns view holder for schedule row. - */ + /** Returns view holder for schedule row. */ protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { return new ScheduleRowViewHolder(view, this); } - /** - * Returns time text for time view from scheduled recording. - */ + /** Returns time text for time view from scheduled recording. */ protected String onGetRecordingTimeText(ScheduleRow row) { - return Utils.getDurationString(mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, - false, true, 0); + return Utils.getDurationString( + mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, false, true, 0); } - /** - * Returns program info text for program title view. - */ + /** Returns program info text for program title view. */ protected String onGetProgramInfoText(ScheduleRow row) { return row.getProgramTitleWithEpisodeNumber(mContext); } private String getChannelNameText(ScheduleRow row) { - Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager() - .getChannel(row.getChannelId()); - return channel == null ? null : - TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : - channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); + Channel channel = + TvSingletons.getSingletons(mContext) + .getChannelDataManager() + .getChannel(row.getChannelId()); + return channel == null + ? null + : TextUtils.isEmpty(channel.getDisplayName()) + ? channel.getDisplayNumber() + : channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); } - /** - * Called when user click Info in {@link ScheduleRow}. - */ + /** Called when user click Info in {@link ScheduleRow}. */ protected void onInfoClicked(ScheduleRow row) { DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); } private boolean isInfoClickable(ScheduleRow row) { - return row.getSchedule() != null - && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress()); + ScheduledRecording schedule = row.getSchedule(); + return schedule != null + && (schedule.isNotStarted() + || schedule.isInProgress() + || schedule.isFinished()); } - /** - * Called when the button in a row is clicked. - */ + /** Called when the button in a row is clicked. */ protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) { switch (action) { case ACTION_START_RECORDING: @@ -481,12 +530,11 @@ class ScheduleRowPresenter extends RowPresenter { case ACTION_REMOVE_SCHEDULE: onRemoveSchedule(row); break; + default: // fall out } } - /** - * Action handler for {@link #ACTION_START_RECORDING}. - */ + /** Action handler for {@link #ACTION_START_RECORDING}. */ protected void onStartRecording(ScheduleRow row) { ScheduledRecording schedule = row.getSchedule(); if (schedule == null) { @@ -495,12 +543,16 @@ class ScheduleRowPresenter extends RowPresenter { } // Checks if there are current recordings that will be stopped by schedule this program. // If so, shows confirmation dialog to users. - List<ScheduledRecording> conflictSchedules = mDvrScheduleManager.getConflictingSchedules( - schedule.getChannelId(), System.currentTimeMillis(), schedule.getEndTimeMs()); + List<ScheduledRecording> conflictSchedules = + mDvrScheduleManager.getConflictingSchedules( + schedule.getChannelId(), + System.currentTimeMillis(), + schedule.getEndTimeMs()); for (int i = conflictSchedules.size() - 1; i >= 0; i--) { ScheduledRecording conflictSchedule = conflictSchedules.get(i); if (conflictSchedule.isInProgress()) { - DvrUiHelper.showStopRecordingDialog((Activity) mContext, + DvrUiHelper.showStopRecordingDialog( + (Activity) mContext, conflictSchedule.getChannelId(), DvrStopRecordingFragment.REASON_ON_CONFLICT, new HalfSizedDialogFragment.OnActionClickListener() { @@ -523,26 +575,27 @@ class ScheduleRowPresenter extends RowPresenter { if (row.isRecordingNotStarted()) { mDvrManager.setHighestPriority(row.getSchedule()); } else if (row.isRecordingFinished()) { - mDvrManager.addSchedule(ScheduledRecording.buildFrom(row.getSchedule()) - .setId(ScheduledRecording.ID_NOT_SET) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) - .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) - .build()); + mDvrManager.addSchedule( + ScheduledRecording.buildFrom(row.getSchedule()) + .setId(ScheduledRecording.ID_NOT_SET) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); } else { - SoftPreconditions.checkState(false, TAG, "Invalid row state to start recording: " - + row); + SoftPreconditions.checkState( + false, TAG, "Invalid row state to start recording: " + row); return; } - String msg = mContext.getString(R.string.dvr_msg_current_program_scheduled, - row.getSchedule().getProgramTitle(), - Utils.toTimeString(row.getEndTimeMs(), false)); + String msg = + mContext.getString( + R.string.dvr_msg_current_program_scheduled, + row.getSchedule().getProgramTitle(), + Utils.toTimeString(row.getEndTimeMs(), false)); ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); } } - /** - * Action handler for {@link #ACTION_STOP_RECORDING}. - */ + /** Action handler for {@link #ACTION_STOP_RECORDING}. */ protected void onStopRecording(ScheduleRow row) { if (row.getSchedule() == null) { // This row has been deleted. @@ -555,15 +608,15 @@ class ScheduleRowPresenter extends RowPresenter { if (TextUtils.isEmpty(deletedInfo)) { deletedInfo = getChannelNameText(row); } - ToastUtils.show(mContext, mContext.getResources() - .getString(R.string.dvr_schedules_deletion_info, deletedInfo), + ToastUtils.show( + mContext, + mContext.getResources() + .getString(R.string.dvr_schedules_deletion_info, deletedInfo), Toast.LENGTH_SHORT); } } - /** - * Action handler for {@link #ACTION_CREATE_SCHEDULE}. - */ + /** Action handler for {@link #ACTION_CREATE_SCHEDULE}. */ protected void onCreateSchedule(ScheduleRow row) { if (row.getSchedule() == null) { // This row has been deleted. @@ -571,12 +624,15 @@ class ScheduleRowPresenter extends RowPresenter { } if (!row.isOnAir()) { if (row.isScheduleCanceled()) { - mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) - .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) - .build()); - String msg = mContext.getString(R.string.dvr_msg_program_scheduled, - row.getSchedule().getProgramTitle()); + mDvrManager.updateScheduledRecording( + ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); + String msg = + mContext.getString( + R.string.dvr_msg_program_scheduled, + row.getSchedule().getProgramTitle()); ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); } else if (mDvrManager.isConflicting(row.getSchedule())) { mDvrManager.setHighestPriority(row.getSchedule()); @@ -584,9 +640,7 @@ class ScheduleRowPresenter extends RowPresenter { } } - /** - * Action handler for {@link #ACTION_REMOVE_SCHEDULE}. - */ + /** Action handler for {@link #ACTION_REMOVE_SCHEDULE}. */ protected void onRemoveSchedule(ScheduleRow row) { if (row.getSchedule() == null) { // This row has been deleted. @@ -605,13 +659,19 @@ class ScheduleRowPresenter extends RowPresenter { mDvrManager.removeScheduledRecording(row.getSchedule()); } else if (row.isRecordingNotStarted()) { deletedInfo = getDeletedInfo(row); - mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) - .setState(ScheduledRecording.STATE_RECORDING_CANCELED) - .build()); + mDvrManager.updateScheduledRecording( + ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_CANCELED) + .build()); + } else if (row.isRecordingFailed()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.removeScheduledRecording(row.getSchedule()); } } if (deletedInfo != null) { - ToastUtils.show(mContext, mContext.getResources() + ToastUtils.show( + mContext, + mContext.getResources() .getString(R.string.dvr_schedules_deletion_info, deletedInfo), Toast.LENGTH_SHORT); } @@ -631,9 +691,7 @@ class ScheduleRowPresenter extends RowPresenter { updateActionContainer(vh, selected); } - /** - * Internal method for onRowViewSelected, can be customized by subclass. - */ + /** Internal method for onRowViewSelected, can be customized by subclass. */ private void updateActionContainer(ViewHolder vh, boolean selected) { ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; viewHolder.mSecondActionContainer.animate().setListener(null).cancel(); @@ -643,38 +701,43 @@ class ScheduleRowPresenter extends RowPresenter { case 2: prepareShowActionView(viewHolder.mSecondActionContainer); prepareShowActionView(viewHolder.mFirstActionContainer); - viewHolder.mPendingAnimationRunnable = new Runnable() { - @Override - public void run() { - showActionView(viewHolder.mSecondActionContainer); - showActionView(viewHolder.mFirstActionContainer); - } - }; + viewHolder.mPendingAnimationRunnable = + new Runnable() { + @Override + public void run() { + showActionView(viewHolder.mSecondActionContainer); + showActionView(viewHolder.mFirstActionContainer); + } + }; break; case 1: prepareShowActionView(viewHolder.mFirstActionContainer); - viewHolder.mPendingAnimationRunnable = new Runnable() { - @Override - public void run() { - hideActionView(viewHolder.mSecondActionContainer, View.GONE); - showActionView(viewHolder.mFirstActionContainer); - } - }; + viewHolder.mPendingAnimationRunnable = + new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + showActionView(viewHolder.mFirstActionContainer); + } + }; if (mLastFocusedViewId == R.id.action_second_container) { mLastFocusedViewId = R.id.info_container; } break; case 0: default: - viewHolder.mPendingAnimationRunnable = new Runnable() { - @Override - public void run() { - hideActionView(viewHolder.mSecondActionContainer, View.GONE); - hideActionView(viewHolder.mFirstActionContainer, View.GONE); - } - }; + viewHolder.mPendingAnimationRunnable = + new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + hideActionView(viewHolder.mFirstActionContainer, View.GONE); + } + }; mLastFocusedViewId = R.id.info_container; - SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG, + SoftPreconditions.checkState( + viewHolder.mInfoContainer.isFocusable(), + TAG, "No focusable view in this row: " + viewHolder); break; } @@ -685,7 +748,7 @@ class ScheduleRowPresenter extends RowPresenter { // requestFocus() explicitly. if (view.hasFocus()) { viewHolder.mPendingAnimationRunnable.run(); - } else if (view.isFocusable()){ + } else if (view.isFocusable()) { view.requestFocus(); } else { viewHolder.view.requestFocus(); @@ -705,17 +768,16 @@ class ScheduleRowPresenter extends RowPresenter { view.setVisibility(View.VISIBLE); } - /** - * Add animation when view is visible. - */ + /** Add animation when view is visible. */ private void showActionView(View view) { - view.animate().alpha(1.0f).setInterpolator(new DecelerateInterpolator()) - .setDuration(mAnimationDuration).start(); + view.animate() + .alpha(1.0f) + .setInterpolator(new DecelerateInterpolator()) + .setDuration(mAnimationDuration) + .start(); } - /** - * Add animation when view change to invisible. - */ + /** Add animation when view change to invisible. */ private void hideActionView(View view, int visibility) { if (view.getVisibility() != View.VISIBLE) { if (view.getVisibility() != visibility) { @@ -723,15 +785,19 @@ class ScheduleRowPresenter extends RowPresenter { } return; } - view.animate().alpha(0.0f).setInterpolator(new DecelerateInterpolator()) + view.animate() + .alpha(0.0f) + .setInterpolator(new DecelerateInterpolator()) .setDuration(mAnimationDuration) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(visibility); - view.animate().setListener(null); - } - }).start(); + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(visibility); + view.animate().setListener(null); + } + }) + .start(); } /** @@ -742,8 +808,8 @@ class ScheduleRowPresenter extends RowPresenter { protected int[] getAvailableActions(ScheduleRow row) { if (row.getSchedule() != null) { if (row.isRecordingInProgress()) { - return new int[]{ACTION_STOP_RECORDING}; - } else if (row.isOnAir()) { + return new int[] {ACTION_STOP_RECORDING}; + } else if (row.isOnAir() && !row.hasRecordedProgram()) { if (row.isRecordingNotStarted()) { if (canResolveConflict()) { // The "START" action can change the conflict states. @@ -754,8 +820,12 @@ class ScheduleRowPresenter extends RowPresenter { } else if (row.isRecordingFinished()) { return new int[] {ACTION_START_RECORDING}; } else { - SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" - + " available actions(on air): " + row); + SoftPreconditions.checkState( + false, + TAG, + "Invalid row state in checking the" + + " available actions(on air): " + + row); } } else { if (row.isScheduleCanceled()) { @@ -764,36 +834,39 @@ class ScheduleRowPresenter extends RowPresenter { return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE}; } else if (row.isRecordingNotStarted()) { return new int[] {ACTION_REMOVE_SCHEDULE}; + } else if (row.isRecordingFailed()) { + return new int[] {ACTION_REMOVE_SCHEDULE}; + } else if (row.isRecordingFinished()) { + return new int[] {}; } else { - SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" - + " available actions(future schedule): " + row); + SoftPreconditions.checkState( + false, + TAG, + "Invalid row state in checking the" + + " available actions(future schedule): " + + row); } } } return null; } - /** - * Check if the conflict can be resolved in this screen. - */ + /** Check if the conflict can be resolved in this screen. */ protected boolean canResolveConflict() { return true; } - /** - * Check if the schedule should be kept after removing it. - */ + /** Check if the schedule should be kept after removing it. */ protected boolean shouldKeepScheduleAfterRemoving() { return false; } - /** - * Checks if the row should be grayed out. - */ + /** Checks if the row should be grayed out. */ protected boolean shouldBeGrayedOut(ScheduleRow row) { return row.getSchedule() == null - || (row.isOnAir() && !row.isRecordingInProgress()) + || (row.isOnAir() && !row.isRecordingInProgress() && !row.hasRecordedProgram()) || mDvrManager.isConflicting(row.getSchedule()) - || row.isScheduleCanceled(); + || row.isScheduleCanceled() + || row.isRecordingFailed(); } } diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index 715ecb8c..bbddc07f 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list; import com.android.tv.data.Program; import com.android.tv.dvr.data.SeriesRecording; - import java.util.List; -/** - * A base class for the rows for schedules' header. - */ +/** A base class for the rows for schedules' header. */ abstract class SchedulesHeaderRow { private String mTitle; private String mDescription; @@ -35,51 +32,37 @@ abstract class SchedulesHeaderRow { mDescription = description; } - /** - * Sets title. - */ + /** Sets title. */ public void setTitle(String title) { mTitle = title; } - /** - * Sets description. - */ + /** Sets description. */ public void setDescription(String description) { mDescription = description; } - /** - * Sets count of items. - */ + /** Sets count of items. */ public void setItemCount(int itemCount) { mItemCount = itemCount; } - /** - * Returns title. - */ + /** Returns title. */ public String getTitle() { return mTitle; } - /** - * Returns description. - */ + /** Returns description. */ public String getDescription() { return mDescription; } - /** - * Returns count of items. - */ + /** Returns count of items. */ public int getItemCount() { return mItemCount; } - /** - * The header row which represent the date. - */ + /** The header row which represent the date. */ public static class DateHeaderRow extends SchedulesHeaderRow { private long mDeadLineMs; @@ -88,47 +71,41 @@ abstract class SchedulesHeaderRow { mDeadLineMs = deadLineMs; } - /** - * Returns the latest time of the list which belongs to the header row. - */ + /** Returns the latest time of the list which belongs to the header row. */ public long getDeadLineMs() { return mDeadLineMs; } } - /** - * The header row which represent the series recording. - */ + /** The header row which represent the series recording. */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { private SeriesRecording mSeriesRecording; private List<Program> mPrograms; - public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series, List<Program> programs) { + public SeriesRecordingHeaderRow( + String title, + String description, + int itemCount, + SeriesRecording series, + List<Program> programs) { super(title, description, itemCount); mSeriesRecording = series; mPrograms = programs; } - /** - * Returns the list of programs which belong to the series. - */ + /** Returns the list of programs which belong to the series. */ public List<Program> getPrograms() { return mPrograms; } - /** - * Returns the series recording, it is for series schedules list. - */ + /** Returns the series recording, it is for series schedules list. */ public SeriesRecording getSeriesRecording() { return mSeriesRecording; } - /** - * Sets the series recording. - */ + /** Sets the series recording. */ public void setSeriesRecording(SeriesRecording seriesRecording) { mSeriesRecording = seriesRecording; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index fe2033ba..eb01aba2 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -27,16 +27,13 @@ import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.TextView; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; -/** - * A base class for RowPresenter for {@link SchedulesHeaderRow} - */ +/** A base class for RowPresenter for {@link SchedulesHeaderRow} */ abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; @@ -46,23 +43,20 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { mContext = context; } - /** - * Returns the context. - */ + /** Returns the context. */ Context getContext() { return mContext; } - /** - * A ViewHolder for {@link SchedulesHeaderRow}. - */ + /** A ViewHolder for {@link SchedulesHeaderRow}. */ public static class SchedulesHeaderRowViewHolder extends RowPresenter.ViewHolder { private TextView mTitle; private TextView mDescription; public SchedulesHeaderRowViewHolder(Context context, ViewGroup parent) { - super(LayoutInflater.from(context).inflate(R.layout.dvr_schedules_header, parent, - false)); + super( + LayoutInflater.from(context) + .inflate(R.layout.dvr_schedules_header, parent, false)); mTitle = (TextView) view.findViewById(R.id.header_title); mDescription = (TextView) view.findViewById(R.id.header_description); } @@ -77,9 +71,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mDescription.setText(header.getDescription()); } - /** - * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. - */ + /** A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter { public DateHeaderRowPresenter(Context context) { super(context); @@ -90,10 +82,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { return new DateHeaderRowViewHolder(getContext(), parent); } - /** - * A ViewHolder for - * {@link SchedulesHeaderRow.DateHeaderRow}. - */ + /** A ViewHolder for {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { public DateHeaderRowViewHolder(Context context, ViewGroup parent) { super(context, parent); @@ -101,9 +90,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { } } - /** - * A presenter for {@link SeriesRecordingHeaderRow}. - */ + /** A presenter for {@link SeriesRecordingHeaderRow}. */ public static class SeriesRecordingHeaderRowPresenter extends SchedulesHeaderRowPresenter { private final boolean mLtr; private final Drawable mSettingsDrawable; @@ -116,8 +103,9 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { public SeriesRecordingHeaderRowPresenter(Context context) { super(context); - mLtr = context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; + mLtr = + context.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; mSettingsDrawable = context.getDrawable(R.drawable.ic_settings); mCancelDrawable = context.getDrawable(R.drawable.ic_dvr_cancel_large); mResumeDrawable = context.getDrawable(R.drawable.ic_record_start); @@ -134,8 +122,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) { super.onBindRowViewHolder(viewHolder, item); - SeriesHeaderRowViewHolder headerViewHolder = - (SeriesHeaderRowViewHolder) viewHolder; + SeriesHeaderRowViewHolder headerViewHolder = (SeriesHeaderRowViewHolder) viewHolder; SeriesRecordingHeaderRow header = (SeriesRecordingHeaderRow) item; headerViewHolder.mSeriesSettingsButton.setVisibility( header.getSeriesRecording().isStopped() ? View.INVISIBLE : View.VISIBLE); @@ -148,46 +135,59 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mToggleStartStopButton.setText(mCancelAllInfo); setTextDrawable(headerViewHolder.mToggleStartStopButton, mCancelDrawable); } - headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), - header.getPrograms(), false, false, false, null); - } - }); - headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - if (header.getSeriesRecording().isStopped()) { - // Reset priority to the highest. - SeriesRecording seriesRecording = SeriesRecording - .buildFrom(header.getSeriesRecording()) - .setPriority(TvApplication.getSingletons(getContext()) - .getDvrScheduleManager().suggestNewSeriesPriority()) - .build(); - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(seriesRecording); - DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), - header.getPrograms(), false, false, false, null); - } else { - DvrUiHelper.showCancelAllSeriesRecordingDialog( - (DvrSchedulesActivity) view.getContext(), - header.getSeriesRecording()); - } - } - }); + headerViewHolder.mSeriesSettingsButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + DvrUiHelper.startSeriesSettingsActivity( + getContext(), + header.getSeriesRecording().getId(), + header.getPrograms(), + false, + false, + false, + null); + } + }); + headerViewHolder.mToggleStartStopButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + if (header.getSeriesRecording().isStopped()) { + // Reset priority to the highest. + SeriesRecording seriesRecording = + SeriesRecording.buildFrom(header.getSeriesRecording()) + .setPriority( + TvSingletons.getSingletons(getContext()) + .getDvrScheduleManager() + .suggestNewSeriesPriority()) + .build(); + TvSingletons.getSingletons(getContext()) + .getDvrManager() + .updateSeriesRecording(seriesRecording); + DvrUiHelper.startSeriesSettingsActivity( + getContext(), + header.getSeriesRecording().getId(), + header.getPrograms(), + false, + false, + false, + null); + } else { + DvrUiHelper.showCancelAllSeriesRecordingDialog( + (DvrSchedulesActivity) view.getContext(), + header.getSeriesRecording()); + } + } + }); } private void setTextDrawable(TextView textView, Drawable drawableStart) { - textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null, - null); + textView.setCompoundDrawablesRelativeWithIntrinsicBounds( + drawableStart, null, null, null); } - /** - * A ViewHolder for {@link SeriesRecordingHeaderRow}. - */ + /** A ViewHolder for {@link SeriesRecordingHeaderRow}. */ public static class SeriesHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { private final TextView mSeriesSettingsButton; private final TextView mToggleStartStopButton; @@ -196,33 +196,40 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { private final View mSelector; private View mLastFocusedView; + public SeriesHeaderRowViewHolder(Context context, ViewGroup parent) { super(context, parent); - mLtr = context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; + mLtr = + context.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; view.findViewById(R.id.button_container).setVisibility(View.VISIBLE); mSeriesSettingsButton = (TextView) view.findViewById(R.id.series_settings); mToggleStartStopButton = (TextView) view.findViewById(R.id.series_toggle_start_stop); mSelector = view.findViewById(R.id.selector); - OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { + OnFocusChangeListener onFocusChangeListener = + new View.OnFocusChangeListener() { @Override - public void run() { - updateSelector(view); + public void onFocusChange(View view, boolean focused) { + view.post( + new Runnable() { + @Override + public void run() { + updateSelector(view); + } + }); } - }); - } - }; + }; mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener); mToggleStartStopButton.setOnFocusChangeListener(onFocusChangeListener); } private void updateSelector(View focusedView) { - int animationDuration = mSelector.getContext().getResources() - .getInteger(android.R.integer.config_shortAnimTime); + int animationDuration = + mSelector + .getContext() + .getResources() + .getInteger(android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); if (focusedView.hasFocus()) { @@ -246,21 +253,38 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { // animate the selector in and to the proper width and translation X. final float deltaWidth = lp.width - targetWidth; mSelector.animate().cancel(); - mSelector.animate().translationX(targetTranslationX).alpha(1f) - .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - // Set width to the proper width for this animation step. - lp.width = targetWidth + Math.round( - deltaWidth * (1f - animation.getAnimatedFraction())); - mSelector.requestLayout(); - } - }).setDuration(animationDuration).setInterpolator(interpolator).start(); + mSelector + .animate() + .translationX(targetTranslationX) + .alpha(1f) + .setUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // Set width to the proper width for this animation + // step. + lp.width = + targetWidth + + Math.round( + deltaWidth + * (1f + - animation + .getAnimatedFraction())); + mSelector.requestLayout(); + } + }) + .setDuration(animationDuration) + .setInterpolator(interpolator) + .start(); mLastFocusedView = focusedView; } else if (mLastFocusedView == focusedView) { mSelector.animate().setUpdateListener(null).cancel(); - mSelector.animate().alpha(0f).setDuration(animationDuration) - .setInterpolator(interpolator).start(); + mSelector + .animate() + .alpha(0f) + .setDuration(animationDuration) + .setInterpolator(interpolator) + .start(); mLastFocusedView = null; } } diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 6b6de8b8..9a9c94ea 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -1,18 +1,18 @@ /* -* 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 -*/ + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ package com.android.tv.dvr.ui.list; @@ -23,10 +23,8 @@ import android.os.Build; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.util.ArrayMap; import android.util.Log; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; @@ -35,16 +33,13 @@ import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; -/** - * An adapter for series schedule row. - */ +/** An adapter for series schedule row. */ @TargetApi(Build.VERSION_CODES.N) class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private static final String TAG = "SeriesRowAdapter"; @@ -57,7 +52,9 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private final Map<Long, Program> mPrograms = new ArrayMap<>(); private SeriesRecordingHeaderRow mHeaderRow; - public SeriesScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector, + public SeriesScheduleRowAdapter( + Context context, + ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording) { super(context, classPresenterSelector); mSeriesRecording = seriesRecording; @@ -67,7 +64,7 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { } else { mInputId = null; } - ApplicationSingletons singletons = TvApplication.getSingletons(context); + TvSingletons singletons = TvSingletons.getSingletons(context); mDvrManager = singletons.getDvrManager(); mDataManager = singletons.getDvrDataManager(); setHasStableIds(true); @@ -83,9 +80,7 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { super.stop(); } - /** - * Sets the programs to show. - */ + /** Sets the programs to show. */ public void setPrograms(List<Program> programs) { if (programs == null) { programs = Collections.emptyList(); @@ -95,8 +90,13 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { List<Program> sortedPrograms = new ArrayList<>(programs); Collections.sort(sortedPrograms); List<EpisodicProgramRow> rows = new ArrayList<>(); - mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - null, sortedPrograms.size(), mSeriesRecording, programs); + mHeaderRow = + new SeriesRecordingHeaderRow( + mSeriesRecording.getTitle(), + null, + sortedPrograms.size(), + mSeriesRecording, + programs); for (Program program : sortedPrograms) { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(program.getId()); @@ -122,8 +122,14 @@ class SeriesScheduleRowAdapter extends ScheduleRowAdapter { ++conflicts; } } - return conflicts == 0 ? null : getContext().getResources().getQuantityString( - R.plurals.dvr_series_schedules_header_description, conflicts, conflicts); + return conflicts == 0 + ? null + : getContext() + .getResources() + .getQuantityString( + R.plurals.dvr_series_schedules_header_description, + conflicts, + conflicts); } @Override diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index c8503e0d..263c579e 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -1,33 +1,30 @@ /* -* 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 -*/ + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ package com.android.tv.dvr.ui.list; import android.content.Context; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; -/** - * A RowPresenter for series schedule row. - */ +/** A RowPresenter for series schedule row. */ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { private static final String TAG = "SeriesRowPresenter"; @@ -35,16 +32,18 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { public SeriesScheduleRowPresenter(Context context) { super(context); - mLtr = context.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; + mLtr = + context.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; } public static class SeriesScheduleRowViewHolder extends ScheduleRowViewHolder { public SeriesScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { super(view, presenter); ViewGroup.LayoutParams lp = getTimeView().getLayoutParams(); - lp.width = view.getResources().getDimensionPixelSize( - R.dimen.dvr_series_schedules_item_time_width); + lp.width = + view.getResources() + .getDimensionPixelSize(R.dimen.dvr_series_schedules_item_time_width); getTimeView().setLayoutParams(lp); } } @@ -56,8 +55,8 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected String onGetRecordingTimeText(ScheduleRow row) { - return Utils.getDurationString(getContext(), row.getStartTimeMs(), row.getEndTimeMs(), - false, true, true, 0); + return Utils.getDurationString( + getContext(), row.getStartTimeMs(), row.getEndTimeMs(), false, true, true, 0); } @Override @@ -71,11 +70,17 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { SeriesScheduleRowViewHolder viewHolder = (SeriesScheduleRowViewHolder) vh; EpisodicProgramRow row = (EpisodicProgramRow) item; if (getDvrManager().isConflicting(row.getSchedule())) { - viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() - .getResources().getDimensionPixelOffset( - R.dimen.dvr_schedules_warning_icon_padding)); - viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds( - R.drawable.ic_warning_gray600_36dp, 0, 0, 0); + viewHolder + .getProgramTitleView() + .setCompoundDrawablePadding( + getContext() + .getResources() + .getDimensionPixelOffset( + R.dimen.dvr_schedules_warning_icon_padding)); + viewHolder + .getProgramTitleView() + .setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_warning_gray600_36dp, 0, 0, 0); } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } @@ -88,16 +93,16 @@ class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected void onStartRecording(ScheduleRow row) { - SoftPreconditions.checkState(row.getSchedule() == null, TAG, - "Start request with the existing schedule: " + row); + SoftPreconditions.checkState( + row.getSchedule() == null, TAG, "Start request with the existing schedule: " + row); row.setStartRecordingRequested(true); getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram()); } @Override protected void onStopRecording(ScheduleRow row) { - SoftPreconditions.checkState(row.getSchedule() != null, TAG, - "Stop request with the null schedule: " + row); + SoftPreconditions.checkState( + row.getSchedule() != null, TAG, "Stop request with the null schedule: " + row); row.setStopRecordingRequested(true); getDvrManager().stopRecording(row.getSchedule()); } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java index 6824cfe2..b8b19adc 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -23,16 +23,13 @@ import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.Log; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.Utils; -/** - * Activity to play a {@link RecordedProgram}. - */ +/** Activity to play a {@link RecordedProgram}. */ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListener { private static final String TAG = "DvrPlaybackActivity"; private static final boolean DEBUG = false; @@ -42,13 +39,14 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene @Override public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); setIntent(createProgramIntent(getIntent())); setContentView(R.layout.activity_dvr_playback); - mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() - .findFragmentById(R.id.dvr_playback_controls_fragment); + mOverlayFragment = + (DvrPlaybackOverlayFragment) + getFragmentManager().findFragmentById(R.id.dvr_playback_controls_fragment); } @Override @@ -68,7 +66,8 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); float density = getResources().getDisplayMetrics().density; - mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), + mOverlayFragment.onWindowSizeChanged( + (int) (newConfig.screenWidthDp * density), (int) (newConfig.screenHeightDp * density)); } @@ -91,4 +90,4 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene void setOnPinCheckListener(OnPinCheckedListener listener) { mOnPinCheckedListener = listener; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java index 8ef0041d..a6352d95 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -17,14 +17,11 @@ package com.android.tv.dvr.ui.playback; import android.content.Context; - import com.android.tv.R; import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; import com.android.tv.dvr.ui.browse.RecordingCardView; -/** - * This class is used to generate Views and bind Objects for related recordings in DVR playback. - */ +/** This class is used to generate Views and bind Objects for related recordings in DVR playback. */ class DvrPlaybackCardPresenter extends RecordedProgramPresenter { private final int mRelatedRecordingCardWidth; private final int mRelatedRecordingCardHeight; @@ -39,7 +36,12 @@ class DvrPlaybackCardPresenter extends RecordedProgramPresenter { @Override public DvrItemViewHolder onCreateDvrItemViewHolder() { - return new RecordedProgramViewHolder(new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true), null); + return new RecordedProgramViewHolder( + new RecordingCardView( + getContext(), + mRelatedRecordingCardWidth, + mRelatedRecordingCardHeight, + true), + null); } } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java index 1a6ae187..59c90d11 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -44,8 +44,8 @@ import com.android.tv.util.TimeShiftUtils; import java.util.ArrayList; /** - * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and - * send command to the media controller. It also helps to update playback states displayed in the + * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and send + * command to the media controller. It also helps to update playback states displayed in the * fragment according to information the media session provides. */ class DvrPlaybackControlHelper extends PlaybackControlGlue { @@ -74,8 +74,9 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mMediaController = activity.getMediaController(); mMediaController.registerCallback(mMediaControllerCallback); mTransportControls = mMediaController.getTransportControls(); - mExtraPaddingTopForNoDescription = activity.getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); + mExtraPaddingTopForNoDescription = + activity.getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); mClosedCaptioningAction = new ClosedCaptioningAction(activity); mMultiAudioAction = new MultiAudioAction(activity); createControlsRowPresenter(); @@ -90,42 +91,47 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private void createControlsRowPresenter() { AbstractDetailsDescriptionPresenter detailsPresenter = new AbstractDetailsDescriptionPresenter() { - @Override - protected void onBindDescription( - AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { - PlaybackControlGlue glue = (PlaybackControlGlue) object; - if (glue.hasValidMedia()) { - viewHolder.getTitle().setText(glue.getMediaTitle()); - viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); - } else { - viewHolder.getTitle().setText(""); - viewHolder.getSubtitle().setText(""); - } - if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { - viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), - mExtraPaddingTopForNoDescription, - viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); - } - } - }; + @Override + protected void onBindDescription( + AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, + Object object) { + PlaybackControlGlue glue = (PlaybackControlGlue) object; + if (glue.hasValidMedia()) { + viewHolder.getTitle().setText(glue.getMediaTitle()); + viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); + } else { + viewHolder.getTitle().setText(""); + viewHolder.getSubtitle().setText(""); + } + if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { + viewHolder.view.setPadding( + viewHolder.view.getPaddingLeft(), + mExtraPaddingTopForNoDescription, + viewHolder.view.getPaddingRight(), + viewHolder.view.getPaddingBottom()); + } + } + }; PlaybackControlsRowPresenter presenter = new PlaybackControlsRowPresenter(detailsPresenter) { - @Override - protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { - super.onBindRowViewHolder(vh, item); - vh.setOnKeyListener(DvrPlaybackControlHelper.this); - } - - @Override - protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { - super.onUnbindRowViewHolder(vh); - vh.setOnKeyListener(null); - } - }; - presenter.setProgressColor(getContext().getResources() - .getColor(R.color.play_controls_progress_bar_watched)); - presenter.setBackgroundColor(getContext().getResources() - .getColor(R.color.play_controls_body_background_enabled)); + @Override + protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + vh.setOnKeyListener(DvrPlaybackControlHelper.this); + } + + @Override + protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { + super.onUnbindRowViewHolder(vh); + vh.setOnKeyListener(null); + } + }; + presenter.setProgressColor( + getContext().getResources().getColor(R.color.play_controls_progress_bar_watched)); + presenter.setBackgroundColor( + getContext() + .getResources() + .getColor(R.color.play_controls_body_background_enabled)); setControlsRowPresenter(presenter); } @@ -166,37 +172,40 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { return false; } int state = playbackState.getState(); - return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING + return state != PlaybackState.STATE_NONE + && state != PlaybackState.STATE_CONNECTING && state != PlaybackState.STATE_PAUSED; } - /** - * Returns the ID of the media under playback. - */ + /** Returns the ID of the media under playback. */ public String getMediaId() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? null + return mediaMetadata == null + ? null : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); } @Override public CharSequence getMediaTitle() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" + return mediaMetadata == null + ? "" : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); } @Override public CharSequence getMediaSubtitle() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" + return mediaMetadata == null + ? "" : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); } @Override public int getMediaDuration() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? 0 + return mediaMetadata == null + ? 0 : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); } @@ -225,19 +234,18 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { return (int) playbackState.getPosition(); } - /** - * Unregister media controller's callback. - */ + /** Unregister media controller's callback. */ void unregisterCallback() { mMediaController.unregisterCallback(mMediaControllerCallback); } /** * Update the secondary controls row. - * @param hasClosedCaption {@code true} to show the closed caption selection button, - * {@code false} to hide it. - * @param hasMultiAudio {@code true} to show the audio track selection button, - * {@code false} to hide it. + * + * @param hasClosedCaption {@code true} to show the closed caption selection button, {@code + * false} to hide it. + * @param hasMultiAudio {@code true} to show the audio track selection button, {@code false} to + * hide it. */ void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { if (hasClosedCaption) { @@ -274,7 +282,7 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls.play(); } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { mTransportControls.rewind(); - } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ + } else if (speedId >= PLAYBACK_SPEED_FAST_L0) { mTransportControls.fastForward(); } } @@ -284,12 +292,10 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls.pause(); } - /** - * Notifies closed caption being enabled/disabled to update related UI. - */ + /** Notifies closed caption being enabled/disabled to update related UI. */ void onSubtitleTrackStateChanged(boolean enabled) { - mClosedCaptioningAction.setIndex(enabled ? - ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); + mClosedCaptioningAction.setIndex( + enabled ? ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); } private void onStateChanged(int state, long positionMs, int speedLevel) { @@ -344,7 +350,9 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); sideFragment.setArguments(args); - mFragment.getFragmentManager().beginTransaction() + mFragment + .getFragmentManager() + .beginTransaction() .hide(mFragment) .replace(R.id.dvr_playback_side_fragment, sideFragment) .addToBackStack(null) @@ -367,7 +375,7 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private static class MultiAudioAction extends MultiAction { MultiAudioAction(Context context) { super(AUDIO_ACTION_ID); - setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); + setDrawables(new Drawable[] {context.getDrawable(R.drawable.ic_tvoption_multi_track)}); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java index 843d2dbe..bef036eb 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -28,17 +28,15 @@ import android.media.tv.TvContract; import android.os.AsyncTask; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; +import com.android.tv.util.images.ImageLoader; class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; @@ -55,49 +53,52 @@ class DvrPlaybackMediaSessionHelper { private final DvrWatchedPositionManager mDvrWatchedPositionManager; private final ChannelDataManager mChannelDataManager; - public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, - DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { + public DvrPlaybackMediaSessionHelper( + Activity activity, + String mediaSessionTag, + DvrPlayer dvrPlayer, + DvrPlaybackOverlayFragment overlayFragment) { mActivity = activity; mDvrPlayer = dvrPlayer; mDvrWatchedPositionManager = - TvApplication.getSingletons(activity).getDvrWatchedPositionManager(); - mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager(); - mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() { - @Override - public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { - updateMediaSessionPlaybackState(); - } + TvSingletons.getSingletons(activity).getDvrWatchedPositionManager(); + mChannelDataManager = TvSingletons.getSingletons(activity).getChannelDataManager(); + mDvrPlayer.setCallback( + new DvrPlayer.DvrPlayerCallback() { + @Override + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { + updateMediaSessionPlaybackState(); + } - @Override - public void onPlaybackPositionChanged(long positionMs) { - updateMediaSessionPlaybackState(); - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrWatchedPositionManager - .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); - } - } + @Override + public void onPlaybackPositionChanged(long positionMs) { + updateMediaSessionPlaybackState(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrWatchedPositionManager.setWatchedPosition( + mDvrPlayer.getProgram().getId(), positionMs); + } + } - @Override - public void onPlaybackEnded() { - // TODO: Deal with watched over recordings in DVR library - RecordedProgram nextEpisode = - overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); - if (nextEpisode == null) { - mDvrPlayer.reset(); - mActivity.finish(); - } else { - Intent intent = new Intent(activity, DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); - mActivity.startActivity(intent); - } - } - }); + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra( + Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } + }); initializeMediaSession(mediaSessionTag); } - /** - * Stops DVR player and release media session. - */ + /** Stops DVR player and release media session. */ public void release() { if (mDvrPlayer != null) { mDvrPlayer.reset(); @@ -108,13 +109,15 @@ class DvrPlaybackMediaSessionHelper { } } - /** - * Updates media session's playback state and speed. - */ + /** Updates media session's playback state and speed. */ public void updateMediaSessionPlaybackState() { - mMediaSession.setPlaybackState(new PlaybackState.Builder() - .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(), - mSpeedLevel).build()); + mMediaSession.setPlaybackState( + new PlaybackState.Builder() + .setState( + mDvrPlayer.getPlaybackState(), + mDvrPlayer.getPlaybackPosition(), + mSpeedLevel) + .build()); } /** @@ -132,42 +135,35 @@ class DvrPlaybackMediaSessionHelper { } } - /** - * Returns the recorded program now playing. - */ + /** Returns the recorded program now playing. */ public RecordedProgram getProgram() { return mDvrPlayer.getProgram(); } - /** - * Checks if the recorded program is the same as now playing one. - */ + /** Checks if the recorded program is the same as now playing one. */ public boolean isCurrentProgram(RecordedProgram program) { return program != null && program.equals(getProgram()); } - /** - * Returns playback state. - */ + /** Returns playback state. */ public int getPlaybackState() { return mDvrPlayer.getPlaybackState(); } - /** - * Returns the underlying DVR player. - */ + /** Returns the underlying DVR player. */ public DvrPlayer getDvrPlayer() { return mDvrPlayer; } private void initializeMediaSession(String mediaSessionTag) { mMediaSession = new MediaSession(mActivity, mediaSessionTag); - mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); - mNowPlayingCardWidth = mActivity.getResources() - .getDimensionPixelSize(R.dimen.notif_card_img_max_width); - mNowPlayingCardHeight = mActivity.getResources() - .getDimensionPixelSize(R.dimen.notif_card_img_height); + mMediaSession.setFlags( + MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mNowPlayingCardWidth = + mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = + mActivity.getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); mMediaSession.setCallback(new MediaSessionCallback()); mActivity.setMediaController( new MediaController(mActivity, mMediaSession.getSessionToken())); @@ -179,11 +175,17 @@ class DvrPlaybackMediaSessionHelper { String cardTitleText = program.getTitle(); if (TextUtils.isEmpty(cardTitleText)) { Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - cardTitleText = (channel != null) ? channel.getDisplayName() - : mActivity.getString(R.string.no_program_information); + cardTitleText = + (channel != null) + ? channel.getDisplayName() + : mActivity.getString(R.string.no_program_information); } - final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, - program.getDescription(), mProgramDurationMs); + final MediaMetadata currentMetadata = + updateMetadataTextInfo( + program.getId(), + cardTitleText, + program.getDescription(), + mProgramDurationMs); String posterArtUri = program.getPosterArtUri(); if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); @@ -192,12 +194,18 @@ class DvrPlaybackMediaSessionHelper { mMediaSession.setActive(true); } - private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, - @Nullable Bitmap posterArt, @Nullable String posterArtUri) { + private void updatePosterArt( + RecordedProgram program, + MediaMetadata currentMetadata, + @Nullable Bitmap posterArt, + @Nullable String posterArtUri) { if (posterArt != null) { updateMetadataImageInfo(program, currentMetadata, posterArt, 0); } else if (posterArtUri != null) { - ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, + ImageLoader.loadBitmap( + mActivity, + posterArtUri, + mNowPlayingCardWidth, mNowPlayingCardHeight, new ProgramPosterArtCallback(mActivity, program, currentMetadata)); } else { @@ -205,13 +213,12 @@ class DvrPlaybackMediaSessionHelper { } } - private class ProgramPosterArtCallback extends - ImageLoader.ImageLoaderCallback<Activity> { + private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> { private final RecordedProgram mRecordedProgram; private final MediaMetadata mCurrentMetadata; - public ProgramPosterArtCallback(Activity activity, RecordedProgram program, - MediaMetadata metadata) { + public ProgramPosterArtCallback( + Activity activity, RecordedProgram program, MediaMetadata metadata) { super(activity); mRecordedProgram = program; mCurrentMetadata = metadata; @@ -225,8 +232,8 @@ class DvrPlaybackMediaSessionHelper { } } - private MediaMetadata updateMetadataTextInfo(final long programId, final String title, - final String subtitle, final long duration) { + private MediaMetadata updateMetadataTextInfo( + final long programId, final String title, final String subtitle, final long duration) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) .putString(MediaMetadata.METADATA_KEY_TITLE, title) @@ -239,8 +246,11 @@ class DvrPlaybackMediaSessionHelper { return metadata; } - private void updateMetadataImageInfo(final RecordedProgram program, - final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { + private void updateMetadataImageInfo( + final RecordedProgram program, + final MediaMetadata currentMetadata, + final Bitmap posterArt, + final int imageResId) { if (mMediaSession != null && (posterArt != null || imageResId != 0)) { MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); if (posterArt != null) { @@ -255,7 +265,8 @@ class DvrPlaybackMediaSessionHelper { @Override protected void onPostExecute(Bitmap programPosterArt) { - if (mMediaSession != null && programPosterArt != null + if (mMediaSession != null + && programPosterArt != null && isCurrentProgram(program)) { builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); mMediaSession.setMetadata(builder.build()); diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java index 783ae682..d3374cfa 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -21,12 +21,12 @@ import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.hardware.display.DisplayManager; -import android.media.tv.TvContentRating; -import android.media.tv.TvTrackInfo; -import android.os.Bundle; import android.media.session.PlaybackState; +import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; import android.media.tv.TvView; +import android.os.Bundle; import android.support.v17.leanback.app.PlaybackFragment; import android.support.v17.leanback.app.PlaybackFragmentGlueHost; import android.support.v17.leanback.widget.ArrayObjectAdapter; @@ -37,14 +37,13 @@ import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.SinglePresenterSelector; +import android.util.Log; import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; -import android.util.Log; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.BaseProgram; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dvr.DvrDataManager; @@ -57,9 +56,8 @@ import com.android.tv.parental.ContentRatingsManager; import com.android.tv.util.TvSettings; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; - -import java.util.List; import java.util.ArrayList; +import java.util.List; public class DvrPlaybackOverlayFragment extends PlaybackFragment { // TODO: Handles audio focus. Deals with block and ratings. @@ -104,15 +102,25 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - mVerticalPaddingBase = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base); - mPaddingWithoutRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); - mPaddingWithoutSecondaryRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getContentRatingsManager(); + mVerticalPaddingBase = + getActivity() + .getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base); + mPaddingWithoutRelatedRow = + getActivity() + .getResources() + .getDimensionPixelOffset( + R.dimen.dvr_playback_overlay_padding_top_no_related_row); + mPaddingWithoutSecondaryRow = + getActivity() + .getResources() + .getDimensionPixelOffset( + R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); + mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); + mContentRatingsManager = + TvSingletons.getSingletons(getContext()) + .getTvInputManagerHelper() + .getContentRatingsManager(); if (!mDvrDataManager.isRecordedProgramLoadFinished()) { mDvrDataManager.addRecordedProgramLoadFinishedListener( new DvrDataManager.OnRecordedProgramLoadFinishedListener() { @@ -124,14 +132,14 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { preparePlayback(getActivity().getIntent()); } } - } - ); + }); } else if (!handleIntent(getActivity().getIntent(), true)) { return; } Point size = new Point(); ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) - .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); + .getDisplay(Display.DEFAULT_DISPLAY) + .getSize(size); mWindowWidth = size.x; mWindowHeight = size.y; mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; @@ -152,19 +160,20 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); mDvrPlayer = new DvrPlayer(mTvView); - mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + mMediaSessionHelper = + new DvrPlaybackMediaSessionHelper( + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); mRelatedRecordingsRow = getRelatedRecordingsRow(); mDvrPlayer.setOnTracksAvailabilityChangedListener( new DvrPlayer.OnTracksAvailabilityChangedListener() { @Override - public void onTracksAvailabilityChanged(boolean hasClosedCaption, - boolean hasMultiAudio) { + public void onTracksAvailabilityChanged( + boolean hasClosedCaption, boolean hasMultiAudio) { mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); if (hasClosedCaption) { - mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, - mOnSubtitleTrackSelectedListener); + mDvrPlayer.setOnTrackSelectedListener( + TvTrackInfo.TYPE_SUBTITLE, mOnSubtitleTrackSelectedListener); selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); } else { mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); @@ -175,15 +184,18 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { updateVerticalPosition(); mPlaybackControlHelper.getHost().notifyPlaybackRowChanged(); } - }); - mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { - @Override - public void onAspectRatioChanged(float videoAspectRatio) { - updateAspectRatio(videoAspectRatio); - } - }); - mPinChecked = getActivity().getIntent() - .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); + }); + mDvrPlayer.setOnAspectRatioChangedListener( + new DvrPlayer.OnAspectRatioChangedListener() { + @Override + public void onAspectRatioChanged(float videoAspectRatio) { + updateAspectRatio(videoAspectRatio); + } + }); + mPinChecked = + getActivity() + .getIntent() + .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); mDvrPlayer.setOnContentBlockedListener( new DvrPlayer.OnContentBlockedListener() { @Override @@ -221,20 +233,25 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { PinDialogFragment.DIALOG_TAG); } }); - setOnItemViewClickedListener(new BaseOnItemViewClickedListener() { - @Override - public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, - RowPresenter.ViewHolder rowViewHolder, Object row) { - if (itemViewHolder.view instanceof RecordingCardView) { - setFadingEnabled(false); - long programId = ((RecordedProgram) itemViewHolder.view.getTag()).getId(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); - } - } - }); + setOnItemViewClickedListener( + new BaseOnItemViewClickedListener() { + @Override + public void onItemClicked( + Presenter.ViewHolder itemViewHolder, + Object item, + RowPresenter.ViewHolder rowViewHolder, + Object row) { + if (itemViewHolder.view instanceof RecordingCardView) { + setFadingEnabled(false); + long programId = + ((RecordedProgram) itemViewHolder.view.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + } + }); if (mProgram != null) { setUpRows(); preparePlayback(getActivity().getIntent()); @@ -265,9 +282,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { super.onDestroy(); } - /** - * Passes the intent to the fragment. - */ + /** Passes the intent to the fragment. */ public void onNewIntent(Intent intent) { if (mDvrDataManager.isRecordedProgramLoadFinished() && handleIntent(intent, false)) { preparePlayback(intent); @@ -275,8 +290,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { } /** - * Should be called when windows' size is changed in order to notify DVR player - * to update it's view width/height and position. + * Should be called when windows' size is changed in order to notify DVR player to update it's + * view width/height and position. */ public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { mWindowWidth = windowWidth; @@ -285,9 +300,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { updateAspectRatio(mAppliedAspectRatio); } - /** - * Returns next recorded episode in the same series as now playing program. - */ + /** Returns next recorded episode in the same series as now playing program. */ public RecordedProgram getNextEpisode(RecordedProgram program) { int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); if (position == mRelatedRecordingsRowAdapter.size()) { @@ -299,9 +312,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { /** * Returns the tracks of the give type of the current playback. - - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. + * + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link + * TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. */ public ArrayList<TvTrackInfo> getTracks(int trackType) { if (trackType == TvTrackInfo.TYPE_AUDIO) { @@ -312,18 +325,16 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { return null; } - /** - * Returns the ID of the selected track of the given type. - */ + /** Returns the ID of the selected track of the given type. */ public String getSelectedTrackId(int trackType) { return mDvrPlayer.getSelectedTrackId(trackType); } /** * Returns the language setting of the given track type. - - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. + * + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link + * TvTrackInfo#TYPE_AUDIO}. * @return {@code null} if no language has been set for the given track type. */ TvTrackInfo getTrackSetting(int trackType) { @@ -332,10 +343,11 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { /** * Selects the given audio or subtitle track for DVR playback. - * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} - * or {@link TvTrackInfo#TYPE_AUDIO}. + * + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} or {@link + * TvTrackInfo#TYPE_AUDIO}. * @param selectedTrack {@code null} to disable the audio or subtitle track according to - * trackType. + * trackType. */ void selectTrack(int trackType, TvTrackInfo selectedTrack) { if (mDvrPlayer.isPlaybackPrepared()) { @@ -346,8 +358,11 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private boolean handleIntent(Intent intent, boolean finishActivity) { mProgram = getProgramFromIntent(intent); if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT) + .show(); if (finishActivity) { getActivity().finish(); } @@ -359,12 +374,18 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private void selectBestMatchedTrack(int trackType) { TvTrackInfo selectedTrack = getTrackSetting(trackType); if (selectedTrack != null) { - TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), - selectedTrack.getId(), selectedTrack.getLanguage(), - trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); - if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils - .isEqualLanguage(bestMatchedTrack.getLanguage(), - selectedTrack.getLanguage()))) { + TvTrackInfo bestMatchedTrack = + TvTrackInfoUtils.getBestTrackInfo( + getTracks(trackType), + selectedTrack.getId(), + selectedTrack.getLanguage(), + trackType == TvTrackInfo.TYPE_AUDIO + ? selectedTrack.getAudioChannelCount() + : 0); + if (bestMatchedTrack != null + && (trackType == TvTrackInfo.TYPE_AUDIO + || Utils.isEqualLanguage( + bestMatchedTrack.getLanguage(), selectedTrack.getLanguage()))) { selectTrack(trackType, bestMatchedTrack); return; } @@ -421,7 +442,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { } if (mRelatedRecordingsRowAdapter.size() == 0) { mRowsAdapter.remove(mRelatedRecordingsRow); - } else if (wasEmpty){ + } else if (wasEmpty) { mRowsAdapter.add(mRelatedRecordingsRow); } updateVerticalPosition(); @@ -446,8 +467,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private ListRow getRelatedRecordingsRow() { mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); - HeaderItem header = new HeaderItem(0, - getActivity().getString(R.string.dvr_playback_related_recordings)); + HeaderItem header = + new HeaderItem( + 0, getActivity().getString(R.string.dvr_playback_related_recordings)); return new ListRow(header, mRelatedRecordingsRowAdapter); } @@ -457,8 +479,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { } private long getSeekTimeFromIntent(Intent intent) { - return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, - TvInputManager.TIME_SHIFT_INVALID_TIME); + return intent.getLongExtra( + Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, TvInputManager.TIME_SHIFT_INVALID_TIME); } private void updateVerticalPosition() { @@ -491,4 +513,4 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { return item.getId(); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java index e49870f1..b4481df8 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java @@ -26,24 +26,16 @@ import android.transition.Transition; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; import com.android.tv.util.TvSettings; - import java.util.List; import java.util.Locale; -/** - * Fragment for DVR playback closed-caption/multi-audio settings. - */ +/** Fragment for DVR playback closed-caption/multi-audio settings. */ public class DvrPlaybackSideFragment extends GuidedStepFragment { - /** - * The tag for passing track infos to side fragments. - */ + /** The tag for passing track infos to side fragments. */ public static final String TRACK_INFOS = "dvr_key_track_infos"; - /** - * The tag for passing selected track's ID to side fragments. - */ + /** The tag for passing selected track's ID to side fragments. */ public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; private static final int ACTION_ID_NO_SUBTITLE = -1; @@ -60,39 +52,42 @@ public class DvrPlaybackSideFragment extends GuidedStepFragment { mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); mTrackType = mTrackInfos.get(0).getType(); mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); - mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() - .findFragmentById(R.id.dvr_playback_controls_fragment)); + mOverlayFragment = + ((DvrPlaybackOverlayFragment) + getFragmentManager().findFragmentById(R.id.dvr_playback_controls_fragment)); super.onCreate(savedInstanceState); } @Override - public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateBackgroundView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); - backgroundView.setBackgroundColor(getResources() - .getColor(R.color.lb_playback_controls_background_light)); + backgroundView.setBackgroundColor( + getResources().getColor(R.color.lb_playback_controls_background_light)); return backgroundView; } @Override public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_NO_SUBTITLE) - .title(getString(R.string.closed_caption_option_item_off)) - .checkSetId(CHECK_SET_ID) - .checked(mSelectedTrackId == null) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_NO_SUBTITLE) + .title(getString(R.string.closed_caption_option_item_off)) + .checkSetId(CHECK_SET_ID) + .checked(mSelectedTrackId == null) + .build()); } for (int i = 0; i < mTrackInfos.size(); i++) { TvTrackInfo info = mTrackInfos.get(i); boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); - GuidedAction action = new GuidedAction.Builder(getActivity()) - .id(i) - .title(getTrackLabel(info, i)) - .checkSetId(CHECK_SET_ID) - .checked(checked) - .build(); + GuidedAction action = + new GuidedAction.Builder(getActivity()) + .id(i) + .title(getTrackLabel(info, i)) + .checkSetId(CHECK_SET_ID) + .checked(checked) + .build(); actions.add(action); if (checked) { mSelectedTrack = info; @@ -136,8 +131,8 @@ public class DvrPlaybackSideFragment extends GuidedStepFragment { if (track.getLanguage() != null) { return new Locale(track.getLanguage()).getDisplayName(); } - return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? - getString(R.string.closed_caption_unknown_language, trackIndex + 1) + return track.getType() == TvTrackInfo.TYPE_SUBTITLE + ? getString(R.string.closed_caption_unknown_language, trackIndex + 1) : getString(R.string.multi_audio_unknown_language); } @@ -151,4 +146,4 @@ public class DvrPlaybackSideFragment extends GuidedStepFragment { t.excludeTarget(R.id.guidedstep_background, true); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java index 7226c666..85bb31b2 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -24,9 +24,7 @@ import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.text.TextUtils; import android.util.Log; - import com.android.tv.dvr.data.RecordedProgram; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -35,17 +33,13 @@ class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; - /** - * The max rewinding speed supported by DVR player. - */ + /** The max rewinding speed supported by DVR player. */ public static final int MAX_REWIND_SPEED = 256; - /** - * The max fast-forwarding speed supported by DVR player. - */ + /** The max fast-forwarding speed supported by DVR player. */ public static final int MAX_FAST_FORWARD_SPEED = 256; private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); - private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 private RecordedProgram mProgram; private long mInitialSeekPositionMs; @@ -71,49 +65,39 @@ class DvrPlayer { public static class DvrPlayerCallback { /** - * Called when the playback position is changed. The normal updating frequency is - * around 1 sec., which is restricted to the implementation of - * {@link android.media.tv.TvInputService}. + * Called when the playback position is changed. The normal updating frequency is around 1 + * sec., which is restricted to the implementation of {@link + * android.media.tv.TvInputService}. */ - public void onPlaybackPositionChanged(long positionMs) { } - /** - * Called when the playback state or the playback speed is changed. - */ - public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } - /** - * Called when the playback toward the end. - */ - public void onPlaybackEnded() { } + public void onPlaybackPositionChanged(long positionMs) {} + /** Called when the playback state or the playback speed is changed. */ + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {} + /** Called when the playback toward the end. */ + public void onPlaybackEnded() {} } public interface OnAspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. * - * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. - * Listeners should handle it carefully. + * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. Listeners + * should handle it carefully. */ void onAspectRatioChanged(float videoAspectRatio); } public interface OnContentBlockedListener { - /** - * Called when the Video's aspect ratio is changed. - */ + /** Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } public interface OnTracksAvailabilityChangedListener { - /** - * Called when the Video's subtitle or audio tracks are changed. - */ + /** Called when the Video's subtitle or audio tracks are changed. */ void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); } public interface OnTrackSelectedListener { - /** - * Called when certain subtitle or audio track is selected. - */ + /** Called when certain subtitle or audio track is selected. */ void onTrackSelected(String selectedTrackId); } @@ -143,9 +127,7 @@ class DvrPlayer { mCallback.onPlaybackStateChanged(mPlaybackState, 1); } - /** - * Resumes playback. - */ + /** Resumes playback. */ public void play() throws IllegalStateException { if (DEBUG) Log.d(TAG, "play()"); if (!isPlaybackPrepared()) { @@ -163,9 +145,7 @@ class DvrPlayer { mCallback.onPlaybackStateChanged(mPlaybackState, 1); } - /** - * Pauses playback. - */ + /** Pauses playback. */ public void pause() throws IllegalStateException { if (DEBUG) Log.d(TAG, "pause()"); if (!isPlaybackPrepared()) { @@ -187,8 +167,8 @@ class DvrPlayer { } /** - * Fast-forwards playback with the given speed. If the given speed is larger than - * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. + * Fast-forwards playback with the given speed. If the given speed is larger than {@value + * #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. */ public void fastForward(int speed) throws IllegalStateException { if (DEBUG) Log.d(TAG, "fastForward()"); @@ -209,8 +189,8 @@ class DvrPlayer { } /** - * Rewinds playback with the given speed. If the given speed is larger than - * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. + * Rewinds playback with the given speed. If the given speed is larger than {@value + * #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. */ public void rewind(int speed) throws IllegalStateException { if (DEBUG) Log.d(TAG, "rewind()"); @@ -230,9 +210,7 @@ class DvrPlayer { mCallback.onPlaybackStateChanged(mPlaybackState, speed); } - /** - * Seeks playback to the specified position. - */ + /** Seeks playback to the specified position. */ public void seekTo(long positionMs) throws IllegalStateException { if (DEBUG) Log.d(TAG, "seekTo()"); if (!isPlaybackPrepared()) { @@ -244,17 +222,15 @@ class DvrPlayer { positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); - if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || - mPlaybackState == PlaybackState.STATE_REWINDING) { + if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING) { mPlaybackState = PlaybackState.STATE_PLAYING; mTvView.timeShiftResume(); mCallback.onPlaybackStateChanged(mPlaybackState, 1); } } - /** - * Resets playback. - */ + /** Resets playback. */ public void reset() { if (DEBUG) Log.d(TAG, "reset()"); mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); @@ -269,9 +245,7 @@ class DvrPlayer { mSelectedSubtitleTrackId = null; } - /** - * Sets callbacks for playback. - */ + /** Sets callbacks for playback. */ public void setCallback(DvrPlayerCallback callback) { if (callback != null) { mCallback = callback; @@ -280,23 +254,17 @@ class DvrPlayer { } } - /** - * Sets the listener to aspect ratio changing. - */ + /** Sets the listener to aspect ratio changing. */ public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { mOnAspectRatioChangedListener = listener; } - /** - * Sets the listener to content blocking. - */ + /** Sets the listener to content blocking. */ public void setOnContentBlockedListener(OnContentBlockedListener listener) { mOnContentBlockedListener = listener; } - /** - * Sets the listener to tracks changing. - */ + /** Sets the listener to tracks changing. */ public void setOnTracksAvailabilityChangedListener( OnTracksAvailabilityChangedListener listener) { mOnTracksAvailabilityChangedListener = listener; @@ -305,8 +273,8 @@ class DvrPlayer { /** * Sets the listener to tracks of the given type being selected. * - * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} - * or {@link TvTrackInfo#TYPE_SUBTITLE}. + * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} or {@link + * TvTrackInfo#TYPE_SUBTITLE}. */ public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { if (trackType == TvTrackInfo.TYPE_AUDIO) { @@ -316,9 +284,7 @@ class DvrPlayer { } } - /** - * Gets the listener to tracks of the given type being selected. - */ + /** Gets the listener to tracks of the given type being selected. */ public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { if (trackType == TvTrackInfo.TYPE_AUDIO) { return mOnAudioTrackSelectedListener; @@ -328,9 +294,7 @@ class DvrPlayer { return null; } - /** - * Sets recorded programs for playback. If the player is playing another program, stops it. - */ + /** Sets recorded programs for playback. If the player is playing another program, stops it. */ public void setProgram(RecordedProgram program, long initialSeekPositionMs) { if (mProgram != null && mProgram.equals(program)) { return; @@ -342,51 +306,37 @@ class DvrPlayer { mProgram = program; } - /** - * Returns the recorded program now playing. - */ + /** Returns the recorded program now playing. */ public RecordedProgram getProgram() { return mProgram; } - /** - * Returns the currrent playback posistion in msecs. - */ + /** Returns the currrent playback posistion in msecs. */ public long getPlaybackPosition() { return mTimeShiftCurrentPositionMs; } - /** - * Returns the playback speed currently used. - */ + /** Returns the playback speed currently used. */ public int getPlaybackSpeed() { return (int) mPlaybackParams.getSpeed(); } - /** - * Returns the playback state defined in {@link android.media.session.PlaybackState}. - */ + /** Returns the playback state defined in {@link android.media.session.PlaybackState}. */ public int getPlaybackState() { return mPlaybackState; } - /** - * Returns the subtitle tracks of the current playback. - */ + /** Returns the subtitle tracks of the current playback. */ public ArrayList<TvTrackInfo> getSubtitleTracks() { return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); } - /** - * Returns the audio tracks of the current playback. - */ + /** Returns the audio tracks of the current playback. */ public ArrayList<TvTrackInfo> getAudioTracks() { return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); } - /** - * Returns the ID of the selected track of the given type. - */ + /** Returns the ID of the selected track of the given type. */ public String getSelectedTrackId(int trackType) { if (trackType == TvTrackInfo.TYPE_AUDIO) { return mSelectedAudioTrackId; @@ -396,9 +346,7 @@ class DvrPlayer { return null; } - /** - * Returns if playback of the recorded program is started. - */ + /** Returns if playback of the recorded program is started. */ public boolean isPlaybackPrepared() { return mPlaybackState != PlaybackState.STATE_NONE && mPlaybackState != PlaybackState.STATE_CONNECTING; @@ -449,125 +397,138 @@ class DvrPlayer { } private void setTvViewCallbacks() { - mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { - @Override - public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { - if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); - mStartPositionMs = timeMs; - if (mTimeShiftPlayAvailable) { - resumeToWatchedPositionIfNeeded(); - } - } + mTvView.setTimeShiftPositionCallback( + new TvView.TimeShiftPositionCallback() { + @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } - @Override - public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { - if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); - if (!mTimeShiftPlayAvailable) { - // Workaround of b/31436263 - return; - } - // Workaround of b/32211561, TIF won't report start position when TIS report - // its start position as 0. In that case, we have to do the prework of playback - // on the first time we get current position, and the start position should be 0 - // at that time. - if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mStartPositionMs = 0; - resumeToWatchedPositionIfNeeded(); - } - timeMs -= mStartPositionMs; - if (mPlaybackState == PlaybackState.STATE_REWINDING - && timeMs <= REWIND_POSITION_MARGIN_MS) { - play(); - } else { - mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); - mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); - if (timeMs >= mProgram.getDurationMillis()) { - pause(); - mCallback.onPlaybackEnded(); + @Override + public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of + // playback + // on the first time we get current position, and the start position should + // be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; + if (mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) { + play(); + } else { + mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); + mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } + } } - } - } - }); - mTvView.setCallback(new TvView.TvInputCallback() { - @Override - public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); - if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE - && mPlaybackState == PlaybackState.STATE_CONNECTING) { - mTimeShiftPlayAvailable = true; - if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - // onTimeShiftStatusChanged is sometimes called after - // onTimeShiftStartPositionChanged is called. In this case, - // resumeToWatchedPositionIfNeeded needs to be called here. - resumeToWatchedPositionIfNeeded(); + }); + mTvView.setCallback( + new TvView.TvInputCallback() { + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); + if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE + && mPlaybackState == PlaybackState.STATE_CONNECTING) { + mTimeShiftPlayAvailable = true; + if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + // onTimeShiftStatusChanged is sometimes called after + // onTimeShiftStartPositionChanged is called. In this case, + // resumeToWatchedPositionIfNeeded needs to be called here. + resumeToWatchedPositionIfNeeded(); + } + } } - } - } - - @Override - public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { - boolean hasClosedCaption = - !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); - boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; - if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) - && mOnTracksAvailabilityChangedListener != null) { - mOnTracksAvailabilityChangedListener - .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); - } - mHasClosedCaption = hasClosedCaption; - mHasMultiAudio = hasMultiAudio; - } - @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { - setSelectedTrackId(type, trackId); - OnTrackSelectedListener listener = getOnTrackSelectedListener(type); - if (listener != null) { - listener.onTrackSelected(trackId); + @Override + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + boolean hasClosedCaption = + !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); + boolean hasMultiAudio = + mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; + if ((hasClosedCaption != mHasClosedCaption + || hasMultiAudio != mHasMultiAudio) + && mOnTracksAvailabilityChangedListener != null) { + mOnTracksAvailabilityChangedListener.onTracksAvailabilityChanged( + hasClosedCaption, hasMultiAudio); + } + mHasClosedCaption = hasClosedCaption; + mHasMultiAudio = hasMultiAudio; } - } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null - && mOnAspectRatioChangedListener != null) { - List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); - if (trackInfos != null) { - for (TvTrackInfo trackInfo : trackInfos) { - if (trackInfo.getId().equals(trackId)) { - float videoAspectRatio; - int videoWidth = trackInfo.getVideoWidth(); - int videoHeight = trackInfo.getVideoHeight(); - if (videoWidth > 0 && videoHeight > 0) { - videoAspectRatio = trackInfo.getVideoPixelAspectRatio() - * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); - } else { - // Aspect ratio is unknown. Pass the message to listeners. - videoAspectRatio = 0; - } - if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); - if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { - mOnAspectRatioChangedListener - .onAspectRatioChanged(videoAspectRatio); - mAspectRatio = videoAspectRatio; - return; + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { + setSelectedTrackId(type, trackId); + OnTrackSelectedListener listener = getOnTrackSelectedListener(type); + if (listener != null) { + listener.onTrackSelected(trackId); + } + } else if (type == TvTrackInfo.TYPE_VIDEO + && trackId != null + && mOnAspectRatioChangedListener != null) { + List<TvTrackInfo> trackInfos = + mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio; + int videoWidth = trackInfo.getVideoWidth(); + int videoHeight = trackInfo.getVideoHeight(); + if (videoWidth > 0 && videoHeight > 0) { + videoAspectRatio = + trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() + / trackInfo.getVideoHeight(); + } else { + // Aspect ratio is unknown. Pass the message to + // listeners. + videoAspectRatio = 0; + } + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (mAspectRatio != videoAspectRatio + || videoAspectRatio == 0) { + mOnAspectRatioChangedListener.onAspectRatioChanged( + videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; + } + } } } } } - } - } - @Override - public void onContentBlocked(String inputId, TvContentRating rating) { - if (mOnContentBlockedListener != null) { - mOnContentBlockedListener.onContentBlocked(rating); - } - } - }); + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); + } + } + }); } private void resumeToWatchedPositionIfNeeded() { if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, - SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mTvView.timeShiftSeekTo( + getRealSeekPosition(mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS) + + mStartPositionMs); mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; } if (mPauseOnPrepared) { @@ -580,4 +541,4 @@ class DvrPlayer { } mCallback.onPlaybackStateChanged(mPlaybackState, 1); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java deleted file mode 100644 index c0cbd643..00000000 --- a/src/com/android/tv/experiments/ExperimentFlag.java +++ /dev/null @@ -1,70 +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.experiments; - -import android.support.annotation.VisibleForTesting; - -/** - * Experiments return values based on user, device and other criteria. - */ -public final class ExperimentFlag<T> { - - private static boolean sAllowOverrides = false; - - @VisibleForTesting - public static void initForTest() { - sAllowOverrides = true; - } - - /** Returns a boolean experiment */ - public static ExperimentFlag<Boolean> createFlag( - boolean defaultValue) { - return new ExperimentFlag<>( - defaultValue); - } - - private final T mDefaultValue; - - private T mOverrideValue = null; - private boolean mOverridden = false; - - private ExperimentFlag( - T defaultValue) { - mDefaultValue = defaultValue; - } - - /** Returns value for this experiment */ - public T get() { - return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue; - } - - @VisibleForTesting - public void override(T t) { - if (sAllowOverrides) { - mOverridden = true; - mOverrideValue = t; - } - } - - @VisibleForTesting - public void resetOverride() { - mOverridden = false; - } - - - -} diff --git a/src/com/android/tv/experiments/Experiments.java b/src/com/android/tv/experiments/Experiments.java deleted file mode 100644 index 53cce979..00000000 --- a/src/com/android/tv/experiments/Experiments.java +++ /dev/null @@ -1,45 +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.experiments; - -import static com.android.tv.experiments.ExperimentFlag.createFlag; - -import com.android.tv.common.BuildConfig; - -/** - * Set of experiments visible in AOSP. - * - * <p>This file is maintained by hand. - */ -public final class Experiments { - public static final ExperimentFlag<Boolean> CLOUD_EPG = createFlag( - true); - - public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS = - createFlag( - false); - - /** - * Allow developer features such as the dev menu and other aids. - * - * <p>These features are available to select users(aka fishfooders) on production builds. - */ - public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES = createFlag( - BuildConfig.ENG); - - private Experiments() {} -} diff --git a/src/com/android/tv/guide/GenreListAdapter.java b/src/com/android/tv/guide/GenreListAdapter.java index ce19eb2d..b4baf421 100644 --- a/src/com/android/tv/guide/GenreListAdapter.java +++ b/src/com/android/tv/guide/GenreListAdapter.java @@ -24,15 +24,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.data.GenreItems; - import java.util.List; -/** - * Adapts the genre items obtained from {@link GenreItems} to the program guide side panel. - */ +/** Adapts the genre items obtained from {@link GenreItems} to the program guide side panel. */ class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHolder> { private static final String TAG = "GenreListAdapter"; private static final boolean DEBUG = false; @@ -45,13 +41,14 @@ class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHol GenreListAdapter(Context context, ProgramManager programManager, ProgramGuide guide) { mContext = context; mProgramManager = programManager; - mProgramManager.addListener(new ProgramManager.ListenerAdapter() { - @Override - public void onGenresUpdated() { - mGenreLabels = GenreItems.getLabels(mContext); - notifyDataSetChanged(); - } - }); + mProgramManager.addListener( + new ProgramManager.ListenerAdapter() { + @Override + public void onGenresUpdated() { + mGenreLabels = GenreItems.getLabels(mContext); + notifyDataSetChanged(); + } + }); mProgramGuide = guide; } @@ -80,23 +77,24 @@ class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHol @Override public GenreRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View view) { - // Animation is not meaningful now, skip it. - view.getStateListAnimator().jumpToCurrentState(); - } - - @Override - public void onViewDetachedFromWindow(View view) { - // Do nothing - } - }); + itemView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + // Animation is not meaningful now, skip it. + view.getStateListAnimator().jumpToCurrentState(); + } + + @Override + public void onViewDetachedFromWindow(View view) { + // Do nothing + } + }); return new GenreRowHolder(itemView, mProgramGuide); } - static class GenreRowHolder extends RecyclerView.ViewHolder implements - View.OnFocusChangeListener { + static class GenreRowHolder extends RecyclerView.ViewHolder + implements View.OnFocusChangeListener { private final ProgramGuide mProgramGuide; private int mGenreId; @@ -119,8 +117,13 @@ class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHol public void onFocusChange(View view, boolean hasFocus) { if (hasFocus) { if (DEBUG) { - Log.d(TAG, "onFocusChanged " + ((TextView) view).getText() - + "(" + mGenreId + ") hasFocus"); + Log.d( + TAG, + "onFocusChanged " + + ((TextView) view).getText() + + "(" + + mGenreId + + ") hasFocus"); } mProgramGuide.requestGenreChange(mGenreId); } diff --git a/src/com/android/tv/guide/GuideUtils.java b/src/com/android/tv/guide/GuideUtils.java index 403d00b5..51c14fd4 100644 --- a/src/com/android/tv/guide/GuideUtils.java +++ b/src/com/android/tv/guide/GuideUtils.java @@ -17,11 +17,9 @@ package com.android.tv.guide; import android.graphics.Rect; -import android.support.annotation.NonNull; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; - import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -30,8 +28,8 @@ class GuideUtils { private static int sWidthPerHour = 0; /** - * Sets the width in pixels that corresponds to an hour in program guide. - * Assume that this is called from main thread only, so, no synchronization. + * Sets the width in pixels that corresponds to an hour in program guide. Assume that this is + * called from main thread only, so, no synchronization. */ static void setWidthPerHour(int widthPerHour) { sWidthPerHour = widthPerHour; @@ -44,30 +42,29 @@ class GuideUtils { return (int) (millis * sWidthPerHour / TimeUnit.HOURS.toMillis(1)); } - /** - * Gets the number of pixels in program guide table that corresponds to the given range. - */ + /** Gets the number of pixels in program guide table that corresponds to the given range. */ static int convertMillisToPixel(long startMillis, long endMillis) { // Convert to pixels first to avoid accumulation of rounding errors. return GuideUtils.convertMillisToPixel(endMillis) - GuideUtils.convertMillisToPixel(startMillis); } - /** - * Gets the time in millis that corresponds to the given pixels in the program guide. - */ + /** Gets the time in millis that corresponds to the given pixels in the program guide. */ static long convertPixelToMillis(int pixel) { return pixel * TimeUnit.HOURS.toMillis(1) / sWidthPerHour; } /** * Return the view should be focused in the given program row according to the focus range. - + * * @param keepCurrentProgramFocused If {@code true}, focuses on the current program if possible, - * else falls back the general logic. + * else falls back the general logic. */ - static View findNextFocusedProgram(View programRow, int focusRangeLeft, - int focusRangeRight, boolean keepCurrentProgramFocused) { + static View findNextFocusedProgram( + View programRow, + int focusRangeLeft, + int focusRangeRight, + boolean keepCurrentProgramFocused) { ArrayList<View> focusables = new ArrayList<>(); findFocusables(programRow, focusables); @@ -102,9 +99,10 @@ class GuideUtils { maxFullyOverlappedWidth = width; } } else if (maxFullyOverlappedWidth == Integer.MIN_VALUE) { - int overlappedWidth = (focusRangeLeft <= focusableRect.left) ? - focusRangeRight - focusableRect.left - : focusableRect.right - focusRangeLeft; + int overlappedWidth = + (focusRangeLeft <= focusableRect.left) + ? focusRangeRight - focusableRect.left + : focusableRect.right - focusRangeLeft; if (overlappedWidth > maxPartiallyOverlappedWidth) { nextFocusIndex = i; maxPartiallyOverlappedWidth = overlappedWidth; @@ -118,16 +116,14 @@ class GuideUtils { } /** - * Returns {@code true} if the program displayed in the give - * {@link com.android.tv.guide.ProgramItemView} is a current program. + * Returns {@code true} if the program displayed in the give {@link + * com.android.tv.guide.ProgramItemView} is a current program. */ static boolean isCurrentProgram(ProgramItemView view) { return view.getTableEntry().isCurrentProgram(); } - /** - * Returns {@code true} if the given view is a descendant of the give container. - */ + /** Returns {@code true} if the given view is a descendant of the give container. */ static boolean isDescendant(ViewGroup container, View view) { if (view == null) { return false; @@ -152,5 +148,5 @@ class GuideUtils { } } - private GuideUtils() { } + private GuideUtils() {} } diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 58436425..caafb045 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -25,15 +25,11 @@ import android.util.Log; import android.util.Range; import android.view.View; import android.view.ViewTreeObserver; - import com.android.tv.R; import com.android.tv.ui.OnRepeatedKeyInterceptListener; - import java.util.concurrent.TimeUnit; -/** - * A {@link VerticalGridView} for the program table view. - */ +/** A {@link VerticalGridView} for the program table view. */ public class ProgramGrid extends VerticalGridView { private static final String TAG = "ProgramGrid"; @@ -84,7 +80,7 @@ public class ProgramGrid extends VerticalGridView { private final int mRowHeight; private final int mDetailHeight; - private final int mSelectionRow; // Row that is focused + private final int mSelectionRow; // Row that is focused private View mLastFocusedView; private final Rect mTempRect = new Rect(); @@ -97,8 +93,8 @@ public class ProgramGrid extends VerticalGridView { interface ChildFocusListener { /** - * Is called before focus is moved. Only children to {@code ProgramGrid} will be passed. - * See {@code ProgramGrid#setChildFocusListener(ChildFocusListener)}. + * Is called before focus is moved. Only children to {@code ProgramGrid} will be passed. See + * {@code ProgramGrid#setChildFocusListener(ChildFocusListener)}. */ void onRequestChildFocus(View oldFocus, View newFocus); } @@ -207,16 +203,13 @@ public class ProgramGrid extends VerticalGridView { } /** - * Initializes ProgramGrid. It should be called before the view is actually attached to - * Window. + * Initializes ProgramGrid. It should be called before the view is actually attached to Window. */ void initialize(ProgramManager programManager) { mProgramManager = programManager; } - /** - * Registers a listener focus events occurring on children to the {@code ProgramGrid}. - */ + /** Registers a listener focus events occurring on children to the {@code ProgramGrid}. */ void setChildFocusListener(ChildFocusListener childFocusListener) { mChildFocusListener = childFocusListener; } @@ -226,8 +219,8 @@ public class ProgramGrid extends VerticalGridView { } /** - * Resets focus states. If the logic to keep the last focus needs to be cleared, it should - * be called. + * Resets focus states. If the logic to keep the last focus needs to be cleared, it should be + * called. */ void resetFocusState() { mLastFocusedView = null; @@ -255,8 +248,8 @@ public class ProgramGrid extends VerticalGridView { Log.w(TAG, "No child view has focus"); return null; } - int nextChildIndex = direction == View.FOCUS_UP ? focusedChildIndex - 1 - : focusedChildIndex + 1; + int nextChildIndex = + direction == View.FOCUS_UP ? focusedChildIndex - 1 : focusedChildIndex + 1; if (nextChildIndex < 0 || nextChildIndex >= getChildCount()) { // Wraparound if reached head or end if (getSelectedPosition() == 0) { @@ -268,8 +261,12 @@ public class ProgramGrid extends VerticalGridView { } return focused; } - View nextFocusedProgram = GuideUtils.findNextFocusedProgram(getChildAt(nextChildIndex), - mFocusRangeLeft, mFocusRangeRight, mKeepCurrentProgramFocused); + View nextFocusedProgram = + GuideUtils.findNextFocusedProgram( + getChildAt(nextChildIndex), + mFocusRangeLeft, + mFocusRangeRight, + mKeepCurrentProgramFocused); if (nextFocusedProgram != null) { nextFocusedProgram.getGlobalVisibleRect(mTempRect); mNextFocusByUpDown = nextFocusedProgram; @@ -320,8 +317,9 @@ public class ProgramGrid extends VerticalGridView { mFocusRangeRight = getRightMostFocusablePosition(); mNextFocusByUpDown = null; // If focus is not a program item, drop focus to the current program when back to the grid - mKeepCurrentProgramFocused = !(focus instanceof ProgramItemView) - || GuideUtils.isCurrentProgram((ProgramItemView) focus); + mKeepCurrentProgramFocused = + !(focus instanceof ProgramItemView) + || GuideUtils.isCurrentProgram((ProgramItemView) focus); } private int getRightMostFocusablePosition() { diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index dd5444e2..5b53f904 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -43,14 +43,14 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; - +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import com.android.tv.ChannelTuner; -import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.util.DurationTimer; +import com.android.tv.TvFeatures; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; +import com.android.tv.common.util.DurationTimer; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; @@ -58,17 +58,16 @@ import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; import com.android.tv.ui.ViewUtils; +import com.android.tv.ui.hideable.AutoHideScheduler; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -/** - * The program guide. - */ -public class ProgramGuide implements ProgramGrid.ChildFocusListener { +/** The program guide. */ +public class ProgramGuide + implements ProgramGrid.ChildFocusListener, AccessibilityStateChangeListener { private static final String TAG = "ProgramGuide"; private static final boolean DEBUG = false; @@ -83,8 +82,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { // We clip out the first program entry in ProgramManager, if it does not have enough width. // In order to prevent from clipping out the current program, this value need be larger than // or equal to ProgramManager.FIRST_ENTRY_MIN_DURATION. - private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME - = ProgramManager.FIRST_ENTRY_MIN_DURATION; + private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME = + ProgramManager.FIRST_ENTRY_MIN_DURATION; private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000; @@ -103,7 +102,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private final long mViewPortMillis; private final int mRowHeight; private final int mDetailHeight; - private final int mSelectionRow; // Row that is focused + private final int mSelectionRow; // Row that is focused private final int mTableFadeAnimDuration; private final int mAnimationDuration; private final int mDetailPadding; @@ -145,34 +144,44 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private final Handler mHandler = new ProgramGuideHandler(this); private boolean mActive; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - hide(); - } - }; + private final AutoHideScheduler mAutoHideScheduler; private final long mShowDurationMillis; private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow; private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener(); - private final Runnable mUpdateTimeIndicator = new Runnable() { - @Override - public void run() { - positionCurrentTimeIndicator(); - mHandler.postAtTime(this, - Utils.ceilTime(SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY)); - } - }; - - public ProgramGuide(MainActivity activity, ChannelTuner channelTuner, - TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, - ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, - @Nullable DvrScheduleManager dvrScheduleManager, Tracker tracker, - Runnable preShowRunnable, Runnable postHideRunnable) { + private final Runnable mUpdateTimeIndicator = + new Runnable() { + @Override + public void run() { + positionCurrentTimeIndicator(); + mHandler.postAtTime( + this, + Utils.ceilTime( + SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY)); + } + }; + + @SuppressWarnings("RestrictTo") + public ProgramGuide( + MainActivity activity, + ChannelTuner channelTuner, + TvInputManagerHelper tvInputManagerHelper, + ChannelDataManager channelDataManager, + ProgramDataManager programDataManager, + @Nullable DvrDataManager dvrDataManager, + @Nullable DvrScheduleManager dvrScheduleManager, + Tracker tracker, + Runnable preShowRunnable, + Runnable postHideRunnable) { mActivity = activity; - mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager, - programDataManager, dvrDataManager, dvrScheduleManager); + mProgramManager = + new ProgramManager( + tvInputManagerHelper, + channelDataManager, + programDataManager, + dvrDataManager, + dvrScheduleManager); mChannelTuner = channelTuner; mTracker = tracker; mPreShowRunnable = preShowRunnable; @@ -185,9 +194,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { Point displaySize = new Point(); mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize); - int gridWidth = displaySize.x - - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start) - - res.getDimensionPixelSize(R.dimen.program_guide_table_header_column_width); + int gridWidth = + displaySize.x + - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start) + - res.getDimensionPixelSize( + R.dimen.program_guide_table_header_column_width); mViewPortMillis = (gridWidth * HOUR_IN_MILLIS) / mWidthPerHour; mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height); @@ -201,43 +212,49 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding); mContainer = mActivity.findViewById(R.id.program_guide); - ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener - = new GlobalFocusChangeListener(); + ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener = + new GlobalFocusChangeListener(); mContainer.getViewTreeObserver().addOnGlobalFocusChangeListener(globalFocusChangeListener); GenreListAdapter genreListAdapter = new GenreListAdapter(mActivity, mProgramManager, this); mSidePanel = mContainer.findViewById(R.id.program_guide_side_panel); - mSidePanelGridView = (VerticalGridView) mContainer.findViewById( - R.id.program_guide_side_panel_grid_view); - mSidePanelGridView.getRecycledViewPool().setMaxRecycledViews( - R.layout.program_guide_side_panel_row, - res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row)); + mSidePanelGridView = + (VerticalGridView) mContainer.findViewById(R.id.program_guide_side_panel_grid_view); + mSidePanelGridView + .getRecycledViewPool() + .setMaxRecycledViews( + R.layout.program_guide_side_panel_row, + res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row)); mSidePanelGridView.setAdapter(genreListAdapter); mSidePanelGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); - mSidePanelGridView.setWindowAlignmentOffset(mActivity.getResources() - .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y)); + mSidePanelGridView.setWindowAlignmentOffset( + mActivity + .getResources() + .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y)); mSidePanelGridView.setWindowAlignmentOffsetPercent( VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); - if (Features.EPG_SEARCH.isEnabled(mActivity)) { - mSearchOrb = (SearchOrbView) mContainer.findViewById( - R.id.program_guide_side_panel_search_orb); + if (TvFeatures.EPG_SEARCH.isEnabled(mActivity)) { + mSearchOrb = + (SearchOrbView) + mContainer.findViewById(R.id.program_guide_side_panel_search_orb); mSearchOrb.setVisibility(View.VISIBLE); - mSearchOrb.setOnOrbClickedListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - hide(); - mActivity.showProgramGuideSearchFragment(); - } - }); + mSearchOrb.setOnOrbClickedListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + hide(); + mActivity.showProgramGuideSearchFragment(); + } + }); mSidePanelGridView.setOnChildSelectedListener( new android.support.v17.leanback.widget.OnChildSelectedListener() { - @Override - public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) { - mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f); - } - }); + @Override + public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) { + mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f); + } + }); } else { mSearchOrb = null; } @@ -246,134 +263,156 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mTimelineRow = (TimelineRow) mTable.findViewById(R.id.time_row); mTimeListAdapter = new TimeListAdapter(res); - mTimelineRow.getRecycledViewPool().setMaxRecycledViews( - R.layout.program_guide_table_header_row_item, - res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item)); + mTimelineRow + .getRecycledViewPool() + .setMaxRecycledViews( + R.layout.program_guide_table_header_row_item, + res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item)); mTimelineRow.setAdapter(mTimeListAdapter); ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, this); - programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - // It is usually called when Genre is changed. - // Reset selection of ProgramGrid - resetRowSelection(); - updateGuidePosition(); - } - }); + programTableAdapter.registerAdapterDataObserver( + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + // It is usually called when Genre is changed. + // Reset selection of ProgramGrid + resetRowSelection(); + updateGuidePosition(); + } + }); mGrid = (ProgramGrid) mTable.findViewById(R.id.grid); mGrid.initialize(mProgramManager); - mGrid.getRecycledViewPool().setMaxRecycledViews( - R.layout.program_guide_table_row, - res.getInteger(R.integer.max_recycled_view_pool_epg_table_row)); + mGrid.getRecycledViewPool() + .setMaxRecycledViews( + R.layout.program_guide_table_row, + res.getInteger(R.integer.max_recycled_view_pool_epg_table_row)); mGrid.setAdapter(programTableAdapter); mGrid.setChildFocusListener(this); - mGrid.setOnChildSelectedListener(new OnChildSelectedListener() { - @Override - public void onChildSelected(ViewGroup parent, View view, int position, long id) { - if (mIsDuringResetRowSelection) { - // Ignore if it's during the first resetRowSelection, because onChildSelected - // will be called again when rows are bound to the program table. if selectRow - // is called here, mSelectedRow is set and the second selectRow call doesn't - // work as intended. - mIsDuringResetRowSelection = false; - return; - } - selectRow(view); - } - }); + mGrid.setOnChildSelectedListener( + new OnChildSelectedListener() { + @Override + public void onChildSelected( + ViewGroup parent, View view, int position, long id) { + if (mIsDuringResetRowSelection) { + // Ignore if it's during the first resetRowSelection, because + // onChildSelected + // will be called again when rows are bound to the program table. if + // selectRow + // is called here, mSelectedRow is set and the second selectRow call + // doesn't + // work as intended. + mIsDuringResetRowSelection = false; + return; + } + selectRow(view); + } + }); mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED); mGrid.setWindowAlignmentOffset(mSelectionRow * mRowHeight); mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); mGrid.setItemAlignmentOffset(0); mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); - RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - onHorizontalScrolled(dx); - } - }; + RecyclerView.OnScrollListener onScrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + onHorizontalScrolled(dx); + } + }; mTimelineRow.addOnScrollListener(onScrollListener); mCurrentTimeIndicator = mTable.findViewById(R.id.current_time_indicator); - mShowAnimatorFull = createAnimator( - R.animator.program_guide_side_panel_enter_full, - 0, - R.animator.program_guide_table_enter_full); - - mShowAnimatorPartial = createAnimator( - R.animator.program_guide_side_panel_enter_partial, - 0, - R.animator.program_guide_table_enter_partial); - mShowAnimatorPartial.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mSidePanelGridView.setVisibility(View.VISIBLE); - mSidePanelGridView.setAlpha(1.0f); - } - }); - - mHideAnimatorFull = createAnimator( - R.animator.program_guide_side_panel_exit, - 0, - R.animator.program_guide_table_exit); - mHideAnimatorFull.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mContainer.setVisibility(View.GONE); - } - }); - mHideAnimatorPartial = createAnimator( - R.animator.program_guide_side_panel_exit, - 0, - R.animator.program_guide_table_exit); - mHideAnimatorPartial.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mContainer.setVisibility(View.GONE); - } - }); - - mPartialToFullAnimator = createAnimator( - R.animator.program_guide_side_panel_hide, - R.animator.program_guide_side_panel_grid_fade_out, - R.animator.program_guide_table_partial_to_full); - mFullToPartialAnimator = createAnimator( - R.animator.program_guide_side_panel_reveal, - R.animator.program_guide_side_panel_grid_fade_in, - R.animator.program_guide_table_full_to_partial); - - mProgramTableFadeOutAnimator = AnimatorInflater.loadAnimator(mActivity, - R.animator.program_guide_table_fade_out); + mShowAnimatorFull = + createAnimator( + R.animator.program_guide_side_panel_enter_full, + 0, + R.animator.program_guide_table_enter_full); + + mShowAnimatorPartial = + createAnimator( + R.animator.program_guide_side_panel_enter_partial, + 0, + R.animator.program_guide_table_enter_partial); + mShowAnimatorPartial.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mSidePanelGridView.setVisibility(View.VISIBLE); + mSidePanelGridView.setAlpha(1.0f); + } + }); + + mHideAnimatorFull = + createAnimator( + R.animator.program_guide_side_panel_exit, + 0, + R.animator.program_guide_table_exit); + mHideAnimatorFull.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mContainer.setVisibility(View.GONE); + } + }); + mHideAnimatorPartial = + createAnimator( + R.animator.program_guide_side_panel_exit, + 0, + R.animator.program_guide_table_exit); + mHideAnimatorPartial.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mContainer.setVisibility(View.GONE); + } + }); + + mPartialToFullAnimator = + createAnimator( + R.animator.program_guide_side_panel_hide, + R.animator.program_guide_side_panel_grid_fade_out, + R.animator.program_guide_table_partial_to_full); + mFullToPartialAnimator = + createAnimator( + R.animator.program_guide_side_panel_reveal, + R.animator.program_guide_side_panel_grid_fade_in, + R.animator.program_guide_table_full_to_partial); + + mProgramTableFadeOutAnimator = + AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_out); mProgramTableFadeOutAnimator.setTarget(mTable); - mProgramTableFadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable) { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - - if (!isActive()) { - return; - } - mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); - resetTimelineScroll(); - if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { - mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM); - } - } - }); - mProgramTableFadeInAnimator = AnimatorInflater.loadAnimator(mActivity, - R.animator.program_guide_table_fade_in); + mProgramTableFadeOutAnimator.addListener( + new HardwareLayerAnimatorListenerAdapter(mTable) { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + + if (!isActive()) { + return; + } + mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); + resetTimelineScroll(); + if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { + mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM); + } + } + }); + mProgramTableFadeInAnimator = + AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_in); mProgramTableFadeInAnimator.setTarget(mTable); mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity); mAccessibilityManager = (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE); - mShowGuidePartial = mAccessibilityManager.isEnabled() - || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); + mShowGuidePartial = + mAccessibilityManager.isEnabled() + || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); + mAutoHideScheduler = new AutoHideScheduler(activity, this::hide); } @Override @@ -397,12 +436,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } /** - * Show the program guide. This reveals the side panel, and the program guide table is shown + * Show the program guide. This reveals the side panel, and the program guide table is shown * partially. * - * <p>Note: the animation which starts together with ProgramGuide showing animation needs to - * be initiated in {@code runnableAfterAnimatorReady}. If the animation starts together - * with show(), the animation may drop some frames. + * <p>Note: the animation which starts together with ProgramGuide showing animation needs to be + * initiated in {@code runnableAfterAnimatorReady}. If the animation starts together with + * show(), the animation may drop some frames. */ public void show(final Runnable runnableAfterAnimatorReady) { if (mContainer.getVisibility() == View.VISIBLE) { @@ -416,9 +455,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mVisibleDuration.start(); mProgramManager.programGuideVisibilityChanged(true); - mStartUtcTime = Utils.floorTime( - System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME, - HALF_HOUR_IN_MILLIS); + mStartUtcTime = + Utils.floorTime( + System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME, + HALF_HOUR_IN_MILLIS); mProgramManager.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); mProgramManager.addListener(mProgramManagerListener); mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; @@ -435,51 +475,60 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { if (DEBUG) { Log.d(TAG, "show()"); } - mOnLayoutListenerForShow = new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); - mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mTable.buildLayer(); - mSidePanelGridView.buildLayer(); - mOnLayoutListenerForShow = null; - mTimelineAnimation = true; - // Make sure that time indicator update starts after animation is finished. - startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); - if (DEBUG) { - mContainer.getViewTreeObserver().addOnDrawListener( - new ViewTreeObserver.OnDrawListener() { - long time = System.currentTimeMillis(); - int count = 0; - - @Override - public void onDraw() { - long curtime = System.currentTimeMillis(); - Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms"); - time = curtime; - if (count > 10) { - mContainer.getViewTreeObserver().removeOnDrawListener(this); - } - } - }); - } - updateGuidePosition(); - runnableAfterAnimatorReady.run(); - if (mShowGuidePartial) { - mShowAnimatorPartial.start(); - } else { - mShowAnimatorFull.start(); - } - } - }; + mOnLayoutListenerForShow = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mTable.buildLayer(); + mSidePanelGridView.buildLayer(); + mOnLayoutListenerForShow = null; + mTimelineAnimation = true; + // Make sure that time indicator update starts after animation is finished. + startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); + if (DEBUG) { + mContainer + .getViewTreeObserver() + .addOnDrawListener( + new ViewTreeObserver.OnDrawListener() { + long time = System.currentTimeMillis(); + int count = 0; + + @Override + public void onDraw() { + long curtime = System.currentTimeMillis(); + Log.d( + TAG, + "onDraw " + + count++ + + " " + + (curtime - time) + + "ms"); + time = curtime; + if (count > 10) { + mContainer + .getViewTreeObserver() + .removeOnDrawListener(this); + } + } + }); + } + updateGuidePosition(); + runnableAfterAnimatorReady.run(); + if (mShowGuidePartial) { + mShowAnimatorPartial.start(); + } else { + mShowAnimatorFull.start(); + } + } + }; mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); scheduleHide(); } - /** - * Hide the program guide. - */ + /** Hide the program guide. */ public void hide() { if (!isActive()) { return; @@ -516,52 +565,43 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } } - /** - * Schedules hiding the program guide. - */ + /** Schedules hiding the program guide. */ public void scheduleHide() { - cancelHide(); - mHandler.postDelayed(mHideRunnable, mShowDurationMillis); + mAutoHideScheduler.schedule(mShowDurationMillis); } - /** - * Cancels hiding the program guide. - */ + /** Cancels hiding the program guide. */ public void cancelHide() { - mHandler.removeCallbacks(mHideRunnable); + mAutoHideScheduler.cancel(); } - /** - * Process the {@code KEYCODE_BACK} key event. - */ + /** Process the {@code KEYCODE_BACK} key event. */ public void onBackPressed() { hide(); } - /** - * Returns {@code true} if the program guide should process the input events. - */ + /** Returns {@code true} if the program guide should process the input events. */ public boolean isActive() { return mActive; } /** - * Returns {@code true} if the program guide is shown, i.e. showing animation is done and - * hiding animation is not started yet. + * Returns {@code true} if the program guide is shown, i.e. showing animation is done and hiding + * animation is not started yet. */ public boolean isRunningAnimation() { - return mShowAnimatorPartial.isStarted() || mShowAnimatorFull.isStarted() - || mHideAnimatorPartial.isStarted() || mHideAnimatorFull.isStarted(); + return mShowAnimatorPartial.isStarted() + || mShowAnimatorFull.isStarted() + || mHideAnimatorPartial.isStarted() + || mHideAnimatorFull.isStarted(); } - /** Returns if program table is in full screen mode. **/ + /** Returns if program table is in full screen mode. * */ boolean isFull() { return !mShowGuidePartial; } - /** - * Requests change genre to {@code genreId}. - */ + /** Requests change genre to {@code genreId}. */ void requestGenreChange(int genreId) { if (mLastRequestedGenreId == genreId) { // When Recycler.onLayout() removes its children to recycle, @@ -575,15 +615,15 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { // When requestGenreChange is called repeatedly in short time, we keep the fade-out // state for mTableFadeAnimDuration from now. Without it, we'll see blinks. mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); - mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM, - mTableFadeAnimDuration); + mHandler.sendEmptyMessageDelayed( + MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration); return; } if (mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); - mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM, - mTableFadeAnimDuration); + mHandler.sendEmptyMessageDelayed( + MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration); return; } if (mProgramTableFadeInAnimator.isStarted()) { @@ -593,9 +633,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mProgramTableFadeOutAnimator.start(); } - /** - * Returns the scroll offset of the time line row in pixels. - */ + /** Returns the scroll offset of the time line row in pixels. */ int getTimelineRowScrollOffset() { return mTimelineRow.getScrollOffset(); } @@ -605,9 +643,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { return mGrid; } - /** - * Gets {@link VerticalGridView} for "genre select" side panel. - */ + /** Gets {@link VerticalGridView} for "genre select" side panel. */ VerticalGridView getSidePanel() { return mSidePanelGridView; } @@ -628,9 +664,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start); int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top); int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom); - int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) - + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding - + bottomPadding; + int tableHeight = + res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) + + mDetailHeight + + mRowHeight * mGrid.getAdapter().getItemCount() + + topPadding + + bottomPadding; if (tableHeight > screenHeight) { // EPG height is longer that the screen height. mTable.setPaddingRelative(startPadding, topPadding, 0, 0); @@ -645,8 +684,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } } - private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, - int tableAnimResId) { + private Animator createAnimator( + int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId) { List<Animator> animatorList = new ArrayList<>(); Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId); @@ -654,8 +693,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { animatorList.add(sidePanelAnimator); if (sidePanelGridAnimResId != 0) { - Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity, - sidePanelGridAnimResId); + Animator sidePanelGridAnimator = + AnimatorInflater.loadAnimator(mActivity, sidePanelGridAnimResId); sidePanelGridAnimator.setTarget(mSidePanelGridView); sidePanelGridAnimator.addListener( new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView)); @@ -700,8 +739,9 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } private void positionCurrentTimeIndicator() { - int offset = GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis()) - - mTimelineRow.getScrollOffset(); + int offset = + GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis()) + - mTimelineRow.getScrollOffset(); if (offset < 0) { mCurrentTimeIndicator.setVisibility(View.GONE); } else { @@ -743,8 +783,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mSelectedRow = null; mIsDuringResetRowSelection = true; mGrid.setSelectedPosition( - Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), - 0)); + Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 0)); mGrid.resetFocusState(); mGrid.onItemSelectionReset(); mIsDuringResetRowSelection = false; @@ -767,12 +806,13 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { detailView.setVisibility(View.VISIBLE); final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row); - programRow.post(new Runnable() { - @Override - public void run() { - programRow.focusCurrentProgram(); - } - }); + programRow.post( + new Runnable() { + @Override + public void run() { + programRow.focusCurrentProgram(); + } + }); } else { animateRowChange(mSelectedRow, row); } @@ -799,38 +839,45 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { if (outDetail != null && outDetail.isShown()) { final View outDetailContent = outDetail.findViewById(R.id.detail_content_full); - Animator fadeOutAnimator = ObjectAnimator.ofPropertyValuesHolder(outDetailContent, - PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f), - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - outDetailContent.getTranslationY(), animationPadding)); + Animator fadeOutAnimator = + ObjectAnimator.ofPropertyValuesHolder( + outDetailContent, + PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f), + PropertyValuesHolder.ofFloat( + View.TRANSLATION_Y, + outDetailContent.getTranslationY(), + animationPadding)); fadeOutAnimator.setStartDelay(0); fadeOutAnimator.setDuration(mAnimationDuration); fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent)); - Animator collapseAnimator = ViewUtils - .createHeightAnimator(outDetail, ViewUtils.getLayoutHeight(outDetail), 0); + Animator collapseAnimator = + ViewUtils.createHeightAnimator( + outDetail, ViewUtils.getLayoutHeight(outDetail), 0); collapseAnimator.setStartDelay(mAnimationDuration); collapseAnimator.setDuration(mTableFadeAnimDuration); - collapseAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - outDetailContent.setVisibility(View.GONE); - } - - @Override - public void onAnimationEnd(Animator animator) { - outDetailContent.setVisibility(View.VISIBLE); - } - }); + collapseAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + outDetailContent.setVisibility(View.GONE); + } + + @Override + public void onAnimationEnd(Animator animator) { + outDetailContent.setVisibility(View.VISIBLE); + } + }); AnimatorSet outAnimator = new AnimatorSet(); outAnimator.playTogether(fadeOutAnimator, collapseAnimator); - outAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mDetailOutAnimator = null; - } - }); + outAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mDetailOutAnimator = null; + } + }); mDetailOutAnimator = outAnimator; outAnimator.start(); } @@ -842,39 +889,49 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { Animator expandAnimator = ViewUtils.createHeightAnimator(inDetail, 0, mDetailHeight); expandAnimator.setStartDelay(mAnimationDuration); expandAnimator.setDuration(mTableFadeAnimDuration); - expandAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - inDetailContent.setVisibility(View.GONE); - } - - @Override - public void onAnimationEnd(Animator animator) { - inDetailContent.setVisibility(View.VISIBLE); - inDetailContent.setAlpha(0); - } - }); - Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent, - PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -animationPadding, 0f)); + expandAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + inDetailContent.setVisibility(View.GONE); + } + + @Override + public void onAnimationEnd(Animator animator) { + inDetailContent.setVisibility(View.VISIBLE); + inDetailContent.setAlpha(0); + } + }); + Animator fadeInAnimator = + ObjectAnimator.ofPropertyValuesHolder( + inDetailContent, + PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), + PropertyValuesHolder.ofFloat( + View.TRANSLATION_Y, -animationPadding, 0f)); fadeInAnimator.setDuration(mAnimationDuration); fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); AnimatorSet inAnimator = new AnimatorSet(); inAnimator.playSequentially(expandAnimator, fadeInAnimator); - inAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mDetailInAnimator = null; - } - }); + inAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mDetailInAnimator = null; + } + }); mDetailInAnimator = inAnimator; inAnimator.start(); } } - private class GlobalFocusChangeListener implements - ViewTreeObserver.OnGlobalFocusChangeListener { + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mAutoHideScheduler.onAccessibilityStateChanged(enabled); + } + + private class GlobalFocusChangeListener + implements ViewTreeObserver.OnGlobalFocusChangeListener { private static final int UNKNOWN = 0; private static final int SIDE_PANEL = 1; private static final int PROGRAM_TABLE = 2; @@ -912,11 +969,16 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private class ProgramManagerListener extends ProgramManager.ListenerAdapter { @Override public void onTimeRangeUpdated() { - int scrollOffset = (int) (mWidthPerHour * mProgramManager.getShiftedTime() - / HOUR_IN_MILLIS); + int scrollOffset = + (int) (mWidthPerHour * mProgramManager.getShiftedTime() / HOUR_IN_MILLIS); if (DEBUG) { - Log.d(TAG, "Horizontal scroll to " + scrollOffset + " pixels (" - + mProgramManager.getShiftedTime() + " millis)"); + Log.d( + TAG, + "Horizontal scroll to " + + scrollOffset + + " pixels (" + + mProgramManager.getShiftedTime() + + " millis)"); } mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation); } diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index b23d578c..9f379e43 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -24,7 +24,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Handler; -import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -35,21 +34,21 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; - -import com.android.tv.ApplicationSingletons; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; +import com.android.tv.common.util.Clock; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; - import java.lang.reflect.InvocationTargetException; import java.util.concurrent.TimeUnit; @@ -60,10 +59,10 @@ public class ProgramItemView extends TextView { private static final int MAX_PROGRESS = 10000; // From android.widget.ProgressBar.MAX_VALUE // State indicating the focused program is the current program - private static final int[] STATE_CURRENT_PROGRAM = { R.attr.state_current_program }; + private static final int[] STATE_CURRENT_PROGRAM = {R.attr.state_current_program}; // Workaround state in order to not use too much texture memory for RippleDrawable - private static final int[] STATE_TOO_WIDE = { R.attr.state_program_too_wide }; + private static final int[] STATE_TOO_WIDE = {R.attr.state_program_too_wide}; private static int sVisibleThreshold; private static int sItemPadding; @@ -73,8 +72,10 @@ public class ProgramItemView extends TextView { private static TextAppearanceSpan sEpisodeTitleStyle; private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle; + private final DvrManager mDvrManager; + private final Clock mClock; + private final ChannelDataManager mChannelDataManager; private ProgramGuide mProgramGuide; - private DvrManager mDvrManager; private TableEntry mTableEntry; private int mMaxWidthForRipple; private int mTextWidth; @@ -84,96 +85,119 @@ public class ProgramItemView extends TextView { // as a result of the re-layout (see b/21378855). private boolean mPreventParentRelayout; - private static final View.OnClickListener ON_CLICKED = new View.OnClickListener() { - @Override - public void onClick(final View view) { - TableEntry entry = ((ProgramItemView) view).mTableEntry; - if (entry == null) { - //do nothing - return; - } - ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext()); - Tracker tracker = singletons.getTracker(); - tracker.sendEpgItemClicked(); - final MainActivity tvActivity = (MainActivity) view.getContext(); - final Channel channel = tvActivity.getChannelDataManager().getChannel(entry.channelId); - if (entry.isCurrentProgram()) { - view.postDelayed(new Runnable() { - @Override - public void run() { - tvActivity.tuneToChannel(channel); - tvActivity.hideOverlaysForTune(); + private static final View.OnClickListener ON_CLICKED = + new View.OnClickListener() { + @Override + public void onClick(final View view) { + TableEntry entry = ((ProgramItemView) view).mTableEntry; + Clock clock = ((ProgramItemView) view).mClock; + if (entry == null) { + // do nothing + return; } - }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 - : view.getResources() - .getInteger(R.integer.program_guide_ripple_anim_duration)); - } else if (entry.program != null && CommonFeatures.DVR.isEnabled(view.getContext())) { - DvrManager dvrManager = singletons.getDvrManager(); - if (entry.entryStartUtcMillis > System.currentTimeMillis() - && dvrManager.isProgramRecordable(entry.program)) { - if (entry.scheduledRecording == null) { - DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, - channel.getInputId(), new Runnable() { + TvSingletons singletons = TvSingletons.getSingletons(view.getContext()); + Tracker tracker = singletons.getTracker(); + tracker.sendEpgItemClicked(); + final MainActivity tvActivity = (MainActivity) view.getContext(); + final Channel channel = + tvActivity.getChannelDataManager().getChannel(entry.channelId); + if (entry.isCurrentProgram()) { + view.postDelayed( + new Runnable() { @Override public void run() { - DvrUiHelper.requestRecordingFutureProgram(tvActivity, - entry.program, false); + tvActivity.tuneToChannel(channel); + tvActivity.hideOverlaysForTune(); } - }); - } else { - dvrManager.removeScheduledRecording(entry.scheduledRecording); - String msg = view.getResources().getString( - R.string.dvr_schedules_deletion_info, entry.program.getTitle()); - ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); + }, + entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple + ? 0 + : view.getResources() + .getInteger( + R.integer + .program_guide_ripple_anim_duration)); + } else if (entry.program != null + && CommonFeatures.DVR.isEnabled(view.getContext())) { + DvrManager dvrManager = singletons.getDvrManager(); + if (entry.entryStartUtcMillis > clock.currentTimeMillis() + && dvrManager.isProgramRecordable(entry.program)) { + if (entry.scheduledRecording == null) { + DvrUiHelper.checkStorageStatusAndShowErrorMessage( + tvActivity, + channel.getInputId(), + new Runnable() { + @Override + public void run() { + DvrUiHelper.requestRecordingFutureProgram( + tvActivity, entry.program, false); + } + }); + } else { + dvrManager.removeScheduledRecording(entry.scheduledRecording); + String msg = + view.getResources() + .getString( + R.string.dvr_schedules_deletion_info, + entry.program.getTitle()); + ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); + } + } else { + ToastUtils.show( + view.getContext(), + view.getResources() + .getString(R.string.dvr_msg_cannot_record_program), + Toast.LENGTH_SHORT); + } } - } else { - ToastUtils.show(view.getContext(), view.getResources() - .getString(R.string.dvr_msg_cannot_record_program), Toast.LENGTH_SHORT); } - } - } - }; + }; private static final View.OnFocusChangeListener ON_FOCUS_CHANGED = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (hasFocus) { - ((ProgramItemView) view).mUpdateFocus.run(); - } else { - Handler handler = view.getHandler(); - if (handler != null) { - handler.removeCallbacks(((ProgramItemView) view).mUpdateFocus); + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus) { + ((ProgramItemView) view).mUpdateFocus.run(); + } else { + Handler handler = view.getHandler(); + if (handler != null) { + handler.removeCallbacks(((ProgramItemView) view).mUpdateFocus); + } + } } - } - } - }; - - private final Runnable mUpdateFocus = new Runnable() { - @Override - public void run() { - refreshDrawableState(); - TableEntry entry = mTableEntry; - if (entry == null) { - //do nothing - return; - } - if (entry.isCurrentProgram()) { - Drawable background = getBackground(); - if (!mProgramGuide.isActive() || mProgramGuide.isRunningAnimation()) { - // If program guide is not active or is during showing/hiding, - // the animation is unnecessary, skip it. - background.jumpToCurrentState(); + }; + + private final Runnable mUpdateFocus = + new Runnable() { + @Override + public void run() { + refreshDrawableState(); + TableEntry entry = mTableEntry; + if (entry == null) { + // do nothing + return; + } + if (entry.isCurrentProgram()) { + Drawable background = getBackground(); + if (!mProgramGuide.isActive() || mProgramGuide.isRunningAnimation()) { + // If program guide is not active or is during showing/hiding, + // the animation is unnecessary, skip it. + background.jumpToCurrentState(); + } + int progress = + getProgress( + mClock, entry.entryStartUtcMillis, entry.entryEndUtcMillis); + setProgress(background, R.id.reverse_progress, MAX_PROGRESS - progress); + } + if (getHandler() != null) { + getHandler() + .postAtTime( + this, + Utils.ceilTime( + mClock.uptimeMillis(), FOCUS_UPDATE_FREQUENCY)); + } } - int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis); - setProgress(background, R.id.reverse_progress, MAX_PROGRESS - progress); - } - if (getHandler() != null) { - getHandler().postAtTime(this, - Utils.ceilTime(SystemClock.uptimeMillis(), FOCUS_UPDATE_FREQUENCY)); - } - } - }; + }; public ProgramItemView(Context context) { this(context, null); @@ -187,7 +211,10 @@ public class ProgramItemView extends TextView { super(context, attrs, defStyle); setOnClickListener(ON_CLICKED); setOnFocusChangeListener(ON_FOCUS_CHANGED); - mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + TvSingletons singletons = TvSingletons.getSingletons(getContext()); + mDvrManager = singletons.getDvrManager(); + mChannelDataManager = singletons.getChannelDataManager(); + mClock = singletons.getClock(); } private void initIfNeeded() { @@ -196,35 +223,46 @@ public class ProgramItemView extends TextView { } Resources res = getContext().getResources(); - sVisibleThreshold = res.getDimensionPixelOffset( - R.dimen.program_guide_table_item_visible_threshold); + sVisibleThreshold = + res.getDimensionPixelOffset(R.dimen.program_guide_table_item_visible_threshold); sItemPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_item_padding); - sCompoundDrawablePadding = res.getDimensionPixelOffset( - R.dimen.program_guide_table_item_compound_drawable_padding); - - ColorStateList programTitleColor = ColorStateList.valueOf(res.getColor( - R.color.program_guide_table_item_program_title_text_color, null)); - ColorStateList grayedOutProgramTitleColor = res.getColorStateList( - R.color.program_guide_table_item_grayed_out_program_text_color, null); - ColorStateList episodeTitleColor = ColorStateList.valueOf(res.getColor( - R.color.program_guide_table_item_program_episode_title_text_color, null)); - ColorStateList grayedOutEpisodeTitleColor = ColorStateList.valueOf(res.getColor( - R.color.program_guide_table_item_grayed_out_program_episode_title_text_color, - null)); - int programTitleSize = res.getDimensionPixelSize( - R.dimen.program_guide_table_item_program_title_font_size); - int episodeTitleSize = res.getDimensionPixelSize( - R.dimen.program_guide_table_item_program_episode_title_font_size); - - sProgramTitleStyle = new TextAppearanceSpan(null, 0, programTitleSize, programTitleColor, - null); - sGrayedOutProgramTitleStyle = new TextAppearanceSpan(null, 0, programTitleSize, - grayedOutProgramTitleColor, null); - sEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, - null); - sGrayedOutEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, - grayedOutEpisodeTitleColor, null); + sCompoundDrawablePadding = + res.getDimensionPixelOffset( + R.dimen.program_guide_table_item_compound_drawable_padding); + + ColorStateList programTitleColor = + ColorStateList.valueOf( + res.getColor( + R.color.program_guide_table_item_program_title_text_color, null)); + ColorStateList grayedOutProgramTitleColor = + res.getColorStateList( + R.color.program_guide_table_item_grayed_out_program_text_color, null); + ColorStateList episodeTitleColor = + ColorStateList.valueOf( + res.getColor( + R.color.program_guide_table_item_program_episode_title_text_color, + null)); + ColorStateList grayedOutEpisodeTitleColor = + ColorStateList.valueOf( + res.getColor( + R.color + .program_guide_table_item_grayed_out_program_episode_title_text_color, + null)); + int programTitleSize = + res.getDimensionPixelSize(R.dimen.program_guide_table_item_program_title_font_size); + int episodeTitleSize = + res.getDimensionPixelSize( + R.dimen.program_guide_table_item_program_episode_title_font_size); + + sProgramTitleStyle = + new TextAppearanceSpan(null, 0, programTitleSize, programTitleColor, null); + sGrayedOutProgramTitleStyle = + new TextAppearanceSpan(null, 0, programTitleSize, grayedOutProgramTitleColor, null); + sEpisodeTitleStyle = + new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, null); + sGrayedOutEpisodeTitleStyle = + new TextAppearanceSpan(null, 0, episodeTitleSize, grayedOutEpisodeTitleColor, null); } @Override @@ -236,8 +274,9 @@ public class ProgramItemView extends TextView { @Override protected int[] onCreateDrawableState(int extraSpace) { if (mTableEntry != null) { - int states[] = super.onCreateDrawableState(extraSpace - + STATE_CURRENT_PROGRAM.length + STATE_TOO_WIDE.length); + int[] states = + super.onCreateDrawableState( + extraSpace + STATE_CURRENT_PROGRAM.length + STATE_TOO_WIDE.length); if (mTableEntry.isCurrentProgram()) { mergeDrawableStates(states, STATE_CURRENT_PROGRAM); } @@ -254,86 +293,168 @@ public class ProgramItemView extends TextView { } @SuppressLint("SwitchIntDef") - public void setValues(ProgramGuide programGuide, TableEntry entry, int selectedGenreId, - long fromUtcMillis, long toUtcMillis, String gapTitle) { + public void setValues( + ProgramGuide programGuide, + TableEntry entry, + int selectedGenreId, + long fromUtcMillis, + long toUtcMillis, + String gapTitle) { mProgramGuide = programGuide; mTableEntry = entry; ViewGroup.LayoutParams layoutParams = getLayoutParams(); - layoutParams.width = entry.getWidth(); - setLayoutParams(layoutParams); + if (layoutParams != null) { + // There is no layoutParams in the tests so we skip this + layoutParams.width = entry.getWidth(); + setLayoutParams(layoutParams); + } + String title = mTableEntry.program != null ? mTableEntry.program.getTitle() : null; + if (mTableEntry.isGap()) { + title = gapTitle; + } + if (TextUtils.isEmpty(title)) { + title = getResources().getString(R.string.program_title_for_no_information); + } + updateText(selectedGenreId, title); + updateIcons(); + updateContentDescription(title); + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd(); + // Maximum width for us to use a ripple + mMaxWidthForRipple = GuideUtils.convertMillisToPixel(fromUtcMillis, toUtcMillis); + } - String title = entry.program != null ? entry.program.getTitle() : null; - String episode = entry.program != null ? - entry.program.getEpisodeDisplayTitle(getContext()) : null; + private boolean isEntryWideEnough() { + return mTableEntry != null && mTableEntry.getWidth() >= sVisibleThreshold; + } + + private void updateText(int selectedGenreId, String title) { + if (!isEntryWideEnough()) { + setText(null); + return; + } + + String episode = + mTableEntry.program != null + ? mTableEntry.program.getEpisodeDisplayTitle(getContext()) + : null; TextAppearanceSpan titleStyle = sGrayedOutProgramTitleStyle; TextAppearanceSpan episodeStyle = sGrayedOutEpisodeTitleStyle; + if (mTableEntry.isGap()) { - if (entry.getWidth() < sVisibleThreshold) { - setText(null); + episode = null; + } else if (mTableEntry.hasGenre(selectedGenreId)) { + titleStyle = sProgramTitleStyle; + episodeStyle = sEpisodeTitleStyle; + } + SpannableStringBuilder description = new SpannableStringBuilder(); + description.append(title); + if (!TextUtils.isEmpty(episode)) { + description.append('\n'); + + // Add a 'zero-width joiner'/ZWJ in order to ensure we have the same line height for + // all lines. This is a non-printing character so it will not change the horizontal + // spacing however it will affect the line height. As we ensure the ZWJ has the same + // text style as the title it will make sure the line height is consistent. + description.append('\u200D'); + + int middle = description.length(); + description.append(episode); + + description.setSpan(titleStyle, 0, middle, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + description.setSpan( + episodeStyle, middle, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else { - if (entry.isGap()) { - title = gapTitle; - episode = null; - } else if (entry.hasGenre(selectedGenreId)) { - titleStyle = sProgramTitleStyle; - episodeStyle = sEpisodeTitleStyle; + description.setSpan( + titleStyle, 0, description.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + setText(description); + } + + private void updateIcons() { + // Sets recording icons if needed. + int iconResId = 0; + if (isEntryWideEnough() && mTableEntry.scheduledRecording != null) { + if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { + iconResId = R.drawable.ic_warning_white_18dp; + } else { + switch (mTableEntry.scheduledRecording.getState()) { + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + iconResId = R.drawable.ic_scheduled_recording; + break; + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + iconResId = R.drawable.ic_recording_program; + break; + default: + // leave the iconResId=0 + } } - if (TextUtils.isEmpty(title)) { - title = getResources().getString(R.string.program_title_for_no_information); + } + setCompoundDrawablePadding(iconResId != 0 ? sCompoundDrawablePadding : 0); + setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconResId, 0); + } + + private void updateContentDescription(String title) { + // The content description includes extra information that is displayed on the detail view + Resources resources = getResources(); + String description = title; + // TODO(b/73282818): only say channel name when the row changes + Channel channel = mChannelDataManager.getChannel(mTableEntry.channelId); + if (channel != null) { + description = channel.getDisplayNumber() + " " + description; + } + description += + " " + + Utils.getDurationString( + getContext(), + mClock, + mTableEntry.entryStartUtcMillis, + mTableEntry.entryEndUtcMillis, + true); + Program program = mTableEntry.program; + if (program != null) { + String episodeDescription = program.getEpisodeContentDescription(getContext()); + if (!TextUtils.isEmpty(episodeDescription)) { + description += " " + episodeDescription; } - SpannableStringBuilder description = new SpannableStringBuilder(); - description.append(title); - if (!TextUtils.isEmpty(episode)) { - description.append('\n'); - - // Add a 'zero-width joiner'/ZWJ in order to ensure we have the same line height for - // all lines. This is a non-printing character so it will not change the horizontal - // spacing however it will affect the line height. As we ensure the ZWJ has the same - // text style as the title it will make sure the line height is consistent. - description.append('\u200D'); - - int middle = description.length(); - description.append(episode); - - description.setSpan(titleStyle, 0, middle, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - description.setSpan(episodeStyle, middle, description.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (mTableEntry.scheduledRecording != null) { + if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { + description += + " " + resources.getString(R.string.dvr_epg_program_recording_conflict); } else { - description.setSpan(titleStyle, 0, description.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - setText(description); - - // Sets recording icons if needed. - int iconResId = 0; - if (mTableEntry.scheduledRecording != null) { - if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { - iconResId = R.drawable.ic_warning_white_18dp; - } else { - switch (mTableEntry.scheduledRecording.getState()) { - case ScheduledRecording.STATE_RECORDING_NOT_STARTED: - iconResId = R.drawable.ic_scheduled_recording; - break; - case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: - iconResId = R.drawable.ic_recording_program; - break; - } + switch (mTableEntry.scheduledRecording.getState()) { + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + description += + " " + + resources.getString( + R.string.dvr_epg_program_recording_scheduled); + break; + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + description += + " " + + resources.getString( + R.string.dvr_epg_program_recording_in_progress); + break; + default: + // do nothing } } - setCompoundDrawablePadding(iconResId != 0 ? sCompoundDrawablePadding : 0); - setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconResId, 0); } - measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd(); - // Maximum width for us to use a ripple - mMaxWidthForRipple = GuideUtils.convertMillisToPixel(fromUtcMillis, toUtcMillis); + if (mTableEntry.isBlocked()) { + description += " " + resources.getString(R.string.program_guide_content_locked); + } else if (program != null) { + String programDescription = program.getDescription(); + if (!TextUtils.isEmpty(programDescription)) { + description += " " + programDescription; + } + } + setContentDescription(description); } - /** - * Update programItemView to handle alignments of text. - */ + /** Update programItemView to handle alignments of text. */ public void updateVisibleArea() { View parentView = ((View) getParent()); if (parentView == null) { @@ -341,7 +462,7 @@ public class ProgramItemView extends TextView { } if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) { layoutVisibleArea(parentView.getLeft() - getLeft(), getRight() - parentView.getRight()); - } else { + } else { layoutVisibleArea(getRight() - parentView.getRight(), parentView.getLeft() - getLeft()); } } @@ -349,16 +470,14 @@ public class ProgramItemView extends TextView { /** * Layout title and episode according to visible area. * - * Here's the spec. - * 1. Don't show text if it's shorter than 48dp. - * 2. Try showing whole text in visible area by placing and wrapping text, - * but do not wrap text less than 30min. - * 3. Episode title is visible only if title isn't multi-line. + * <p>Here's the spec. 1. Don't show text if it's shorter than 48dp. 2. Try showing whole text + * in visible area by placing and wrapping text, but do not wrap text less than 30min. 3. + * Episode title is visible only if title isn't multi-line. * * @param startOffset Offset of the start position from the enclosing view's start position. * @param endOffset Offset of the end position from the enclosing view's end position. */ - private void layoutVisibleArea(int startOffset, int endOffset) { + private void layoutVisibleArea(int startOffset, int endOffset) { int width = mTableEntry.getWidth(); int startPadding = Math.max(0, startOffset); int endPadding = Math.max(0, endOffset); @@ -388,8 +507,8 @@ public class ProgramItemView extends TextView { mTableEntry = null; } - private static int getProgress(long start, long end) { - long currentTime = System.currentTimeMillis(); + private static int getProgress(Clock clock, long start, long end) { + long currentTime = clock.currentTimeMillis(); if (currentTime <= start) { return 0; } else if (currentTime >= end) { @@ -417,11 +536,15 @@ public class ProgramItemView extends TextView { private static int getStateCount(StateListDrawable stateListDrawable) { try { - Object stateCount = StateListDrawable.class.getDeclaredMethod("getStateCount") - .invoke(stateListDrawable); + Object stateCount = + StateListDrawable.class + .getDeclaredMethod("getStateCount") + .invoke(stateListDrawable); return (int) stateCount; - } catch (NoSuchMethodException|IllegalAccessException|IllegalArgumentException - |InvocationTargetException e) { + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { Log.e(TAG, "Failed to call StateListDrawable.getStateCount()", e); return 0; } @@ -429,12 +552,15 @@ public class ProgramItemView extends TextView { private static Drawable getStateDrawable(StateListDrawable stateListDrawable, int index) { try { - Object drawable = StateListDrawable.class - .getDeclaredMethod("getStateDrawable", Integer.TYPE) - .invoke(stateListDrawable, index); + Object drawable = + StateListDrawable.class + .getDeclaredMethod("getStateDrawable", Integer.TYPE) + .invoke(stateListDrawable, index); return (Drawable) drawable; - } catch (NoSuchMethodException|IllegalAccessException|IllegalArgumentException - |InvocationTargetException e) { + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { Log.e(TAG, "Failed to call StateListDrawable.getStateDrawable(" + index + ")", e); return null; } diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java index c1fcdd40..397bacfb 100644 --- a/src/com/android/tv/guide/ProgramListAdapter.java +++ b/src/com/android/tv/guide/ProgramListAdapter.java @@ -22,9 +22,8 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.guide.ProgramManager.TableEntry; @@ -111,9 +110,14 @@ class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.Program Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry); } ProgramManager programManager = programGuide.getProgramManager(); - ((ProgramItemView) itemView).setValues(programGuide, entry, - programManager.getSelectedGenreId(), programManager.getFromUtcMillis(), - programManager.getToUtcMillis(), gapTitle); + ((ProgramItemView) itemView) + .setValues( + programGuide, + entry, + programManager.getSelectedGenreId(), + programManager.getFromUtcMillis(), + programManager.getToUtcMillis(), + gapTitle); } void onUnbind() { diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index 4ec3f77e..3f20a837 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -18,21 +18,20 @@ package com.android.tv.guide; import android.support.annotation.MainThread; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.util.ArraySet; import android.util.Log; - -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -40,9 +39,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -/** - * Manages the channels and programs for the program guide. - */ +/** Manages the channels and programs for the program guide. */ @MainThread public class ProgramManager { private static final String TAG = "ProgramManager"; @@ -60,7 +57,7 @@ public class ProgramManager { private final TvInputManagerHelper mTvInputManagerHelper; private final ChannelDataManager mChannelDataManager; private final ProgramDataManager mProgramDataManager; - private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled + private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled private final DvrScheduleManager mDvrScheduleManager; private long mStartUtcMillis; @@ -127,51 +124,67 @@ public class ProgramManager { private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = new DvrDataManager.ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording schedule : scheduledRecordings) { - TableEntry oldEntry = getTableEntry(schedule); - if (oldEntry != null) { - TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, - schedule, oldEntry.entryStartUtcMillis, - oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); - updateEntry(oldEntry, newEntry); + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording schedule : scheduledRecordings) { + TableEntry oldEntry = getTableEntry(schedule); + if (oldEntry != null) { + TableEntry newEntry = + new TableEntry( + oldEntry.channelId, + oldEntry.program, + schedule, + oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, + oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } } - } - } - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording schedule : scheduledRecordings) { - TableEntry oldEntry = getTableEntry(schedule); - if (oldEntry != null) { - TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, null, - oldEntry.entryStartUtcMillis, oldEntry.entryEndUtcMillis, - oldEntry.isBlocked()); - updateEntry(oldEntry, newEntry); + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording schedule : scheduledRecordings) { + TableEntry oldEntry = getTableEntry(schedule); + if (oldEntry != null) { + TableEntry newEntry = + new TableEntry( + oldEntry.channelId, + oldEntry.program, + null, + oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, + oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } } - } - } - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording schedule : scheduledRecordings) { - TableEntry oldEntry = getTableEntry(schedule); - if (oldEntry != null) { - TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, - schedule, oldEntry.entryStartUtcMillis, - oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); - updateEntry(oldEntry, newEntry); + @Override + public void onScheduledRecordingStatusChanged( + ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording schedule : scheduledRecordings) { + TableEntry oldEntry = getTableEntry(schedule); + if (oldEntry != null) { + TableEntry newEntry = + new TableEntry( + oldEntry.channelId, + oldEntry.program, + schedule, + oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, + oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } } - } - } - }; + }; private final OnConflictStateChangeListener mOnConflictStateChangeListener = new OnConflictStateChangeListener() { @Override - public void onConflictStateChange(boolean conflict, - ScheduledRecording... schedules) { + public void onConflictStateChange( + boolean conflict, ScheduledRecording... schedules) { for (ScheduledRecording schedule : schedules) { TableEntry entry = getTableEntry(schedule); if (entry != null) { @@ -181,8 +194,10 @@ public class ProgramManager { } }; - public ProgramManager(TvInputManagerHelper tvInputManagerHelper, - ChannelDataManager channelDataManager, ProgramDataManager programDataManager, + public ProgramManager( + TvInputManagerHelper tvInputManagerHelper, + ChannelDataManager channelDataManager, + ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, @Nullable DvrScheduleManager dvrScheduleManager) { mTvInputManagerHelper = tvInputManagerHelper; @@ -221,52 +236,39 @@ public class ProgramManager { } } - /** - * Adds a {@link Listener}. - */ + /** Adds a {@link Listener}. */ void addListener(Listener listener) { mListeners.add(listener); } - /** - * Registers a listener to be invoked when table entries are updated. - */ + /** Registers a listener to be invoked when table entries are updated. */ void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.add(listener); } - /** - * Registers a listener to be invoked when a table entry is changed. - */ + /** Registers a listener to be invoked when a table entry is changed. */ void addTableEntryChangedListener(TableEntryChangedListener listener) { mTableEntryChangedListeners.add(listener); } - /** - * Removes a {@link Listener}. - */ + /** Removes a {@link Listener}. */ void removeListener(Listener listener) { mListeners.remove(listener); } - /** - * Removes a previously installed table entries update listener. - */ + /** Removes a previously installed table entries update listener. */ void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.remove(listener); } - /** - * Removes a previously installed table entry changed listener. - */ + /** Removes a previously installed table entry changed listener. */ void removeTableEntryChangedListener(TableEntryChangedListener listener) { mTableEntryChangedListeners.remove(listener); } /** - * Resets channel list with given genre. - * Caller should call {@link #buildGenreFilters()} prior to call this API to make - * This notifies channel updates to listeners. + * Resets channel list with given genre. Caller should call {@link #buildGenreFilters()} prior + * to call this API to make This notifies channel updates to listeners. */ void resetChannelListWithGenre(int genreId) { if (genreId == mSelectedGenreId) { @@ -275,8 +277,14 @@ public class ProgramManager { mFilteredChannels = mGenreChannelList.get(genreId); mSelectedGenreId = genreId; if (DEBUG) { - Log.d(TAG, "resetChannelListWithGenre: " + GenreItems.getCanonicalGenre(genreId) - + " has " + mFilteredChannels.size() + " channels out of " + mChannels.size()); + Log.d( + TAG, + "resetChannelListWithGenre: " + + GenreItems.getCanonicalGenre(genreId) + + " has " + + mFilteredChannels.size() + + " channels out of " + + mChannels.size()); } if (mGenreChannelList.get(mSelectedGenreId) == null) { throw new IllegalStateException("Genre filter isn't ready."); @@ -284,9 +292,7 @@ public class ProgramManager { notifyChannelsUpdated(); } - /** - * Update the initial time range to manage. It updates program entries and genre as well. - */ + /** Update the initial time range to manage. It updates program entries and genre as well. */ void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) { mStartUtcMillis = startUtcMillis; if (endUtcMillis > mEndUtcMillis) { @@ -298,10 +304,7 @@ public class ProgramManager { setTimeRange(startUtcMillis, endUtcMillis); } - - /** - * Shifts the time range by the given time. Also makes ProgramGuide scroll the views. - */ + /** Shifts the time range by the given time. Also makes ProgramGuide scroll the views. */ void shiftTime(long timeMillisToScroll) { long fromUtcMillis = mFromUtcMillis + timeMillisToScroll; long toUtcMillis = mToUtcMillis + timeMillisToScroll; @@ -316,23 +319,17 @@ public class ProgramManager { setTimeRange(fromUtcMillis, toUtcMillis); } - /** - * Returned the scrolled(shifted) time in milliseconds. - */ + /** Returned the scrolled(shifted) time in milliseconds. */ long getShiftedTime() { return mFromUtcMillis - mStartUtcMillis; } - /** - * Returns the start time set by {@link #updateInitialTimeRange}. - */ + /** Returns the start time set by {@link #updateInitialTimeRange}. */ long getStartTime() { return mStartUtcMillis; } - /** - * Returns the program index of the program with {@code entryId} or -1 if not found. - */ + /** Returns the program index of the program with {@code entryId} or -1 if not found. */ int getProgramIdIndex(long channelId, long entryId) { List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); if (entries != null) { @@ -345,38 +342,29 @@ public class ProgramManager { return -1; } - /** - * Returns the program index of the program at {@code time} or -1 if not found. - */ + /** Returns the program index of the program at {@code time} or -1 if not found. */ int getProgramIndexAtTime(long channelId, long time) { List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); for (int i = 0; i < entries.size(); ++i) { TableEntry entry = entries.get(i); - if (entry.entryStartUtcMillis <= time - && time < entry.entryEndUtcMillis) { + if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) { return i; } } return -1; } - /** - * Returns the start time of currently managed time range, in UTC millisecond. - */ + /** Returns the start time of currently managed time range, in UTC millisecond. */ long getFromUtcMillis() { return mFromUtcMillis; } - /** - * Returns the end time of currently managed time range, in UTC millisecond. - */ + /** Returns the end time of currently managed time range, in UTC millisecond. */ long getToUtcMillis() { return mToUtcMillis; } - /** - * Returns the number of the currently managed channels. - */ + /** Returns the number of the currently managed channels. */ int getChannelCount() { return mFilteredChannels.size(); } @@ -393,15 +381,15 @@ public class ProgramManager { } /** - * Returns the index of provided {@link Channel} within the currently managed channels. - * Returns -1 if such a channel is not found. + * Returns the index of provided {@link Channel} within the currently managed channels. Returns + * -1 if such a channel is not found. */ int getChannelIndex(Channel channel) { return mFilteredChannels.indexOf(channel); } /** - * Returns the index of channel with {@code channelId} within the currently managed channels. + * Returns the index of channel with {@code channelId} within the currently managed channels. * Returns -1 if such a channel is not found. */ int getChannelIndex(long channelId) { @@ -425,9 +413,7 @@ public class ProgramManager { return mChannelIdEntriesMap.get(channelId).get(index); } - /** - * Returns list genre ID's which has a channel. - */ + /** Returns list genre ID's which has a channel. */ List<Integer> getFilteredGenreIds() { return mFilteredGenreIds; } @@ -457,15 +443,13 @@ public class ProgramManager { buildGenreFilters(); } - /** - * Updates the table entries without notifying the change. - */ + /** Updates the table entries without notifying the change. */ private void updateTableEntriesWithoutNotification(boolean clear) { if (clear) { mChannelIdEntriesMap.clear(); } - boolean parentalControlsEnabled = mTvInputManagerHelper.getParentalControlSettings() - .isParentalControlsEnabled(); + boolean parentalControlsEnabled = + mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled(); for (Channel channel : mChannels) { long channelId = channel.getId(); // Inline the updating of the mChannelIdEntriesMap here so we can only call @@ -475,8 +459,12 @@ public class ProgramManager { int size = entries.size(); if (DEBUG) { - Log.d(TAG, "Programs are loaded for channel " + channel.getId() - + ", loaded size = " + size); + Log.d( + TAG, + "Programs are loaded for channel " + + channel.getId() + + ", loaded size = " + + size); } if (size == 0) { continue; @@ -496,14 +484,19 @@ public class ProgramManager { } else { TableEntry lastEntry = entries.get(entries.size() - 1); if (mEndUtcMillis > lastEntry.entryEndUtcMillis) { - entries.add(new TableEntry(channelId, lastEntry.entryEndUtcMillis, - mEndUtcMillis)); + entries.add( + new TableEntry( + channelId, lastEntry.entryEndUtcMillis, mEndUtcMillis)); } else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) { entries.remove(entries.size() - 1); - entries.add(new TableEntry(lastEntry.channelId, lastEntry.program, - lastEntry.scheduledRecording, - lastEntry.entryStartUtcMillis, mEndUtcMillis, - lastEntry.mIsBlocked)); + entries.add( + new TableEntry( + lastEntry.channelId, + lastEntry.program, + lastEntry.scheduledRecording, + lastEntry.entryStartUtcMillis, + mEndUtcMillis, + lastEntry.mIsBlocked)); } } } @@ -511,11 +504,10 @@ public class ProgramManager { } /** - * Build genre filters based on the current programs. - * This categories channels by its current program's canonical genres - * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list - * with built channel list. - * This is expected to be called whenever program guide is shown. + * Build genre filters based on the current programs. This categories channels by its current + * program's canonical genres and subsequent @{link resetChannelListWithGenre(int)} calls will + * reset channel list with built channel list. This is expected to be called whenever program + * guide is shown. */ private void buildGenreFilters() { if (DEBUG) Log.d(TAG, "buildGenreFilters"); @@ -572,9 +564,13 @@ public class ProgramManager { private void setTimeRange(long fromUtcMillis, long toUtcMillis) { if (DEBUG) { - Log.d(TAG, "setTimeRange. {FromTime=" - + Utils.toTimeString(fromUtcMillis) + ", ToTime=" - + Utils.toTimeString(toUtcMillis) + "}"); + Log.d( + TAG, + "setTimeRange. {FromTime=" + + Utils.toTimeString(fromUtcMillis) + + ", ToTime=" + + Utils.toTimeString(toUtcMillis) + + "}"); } if (mFromUtcMillis != fromUtcMillis || mToUtcMillis != toUtcMillis) { mFromUtcMillis = fromUtcMillis; @@ -585,8 +581,8 @@ public class ProgramManager { private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) { List<TableEntry> entries = new ArrayList<>(); - boolean channelLocked = parentalControlsEnabled - && mChannelDataManager.getChannel(channelId).isLocked(); + boolean channelLocked = + parentalControlsEnabled && mChannelDataManager.getChannel(channelId).isLocked(); if (channelLocked) { entries.add(new TableEntry(channelId, mStartUtcMillis, Long.MAX_VALUE, true)); } else { @@ -597,20 +593,27 @@ public class ProgramManager { // Dummy program. continue; } - long programStartTime = Math.max(program.getStartTimeUtcMillis(), - mStartUtcMillis); + long programStartTime = Math.max(program.getStartTimeUtcMillis(), mStartUtcMillis); long programEndTime = program.getEndTimeUtcMillis(); if (programStartTime > lastProgramEndTime) { // Gap since the last program. - entries.add(new TableEntry(channelId, lastProgramEndTime, - programStartTime)); + entries.add(new TableEntry(channelId, lastProgramEndTime, programStartTime)); lastProgramEndTime = programStartTime; } if (programEndTime > lastProgramEndTime) { - ScheduledRecording scheduledRecording = mDvrDataManager == null ? null - : mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); - entries.add(new TableEntry(channelId, program, scheduledRecording, - lastProgramEndTime, programEndTime, false)); + ScheduledRecording scheduledRecording = + mDvrDataManager == null + ? null + : mDvrDataManager.getScheduledRecordingForProgramId( + program.getId()); + entries.add( + new TableEntry( + channelId, + program, + scheduledRecording, + lastProgramEndTime, + programEndTime, + false)); lastProgramEndTime = programEndTime; } } @@ -622,9 +625,15 @@ public class ProgramManager { // If the first entry's width doesn't have enough width, it is not good to show // the first entry from UI perspective. So we clip it out. entries.remove(0); - entries.set(0, new TableEntry(secondEntry.channelId, secondEntry.program, - secondEntry.scheduledRecording, mStartUtcMillis, - secondEntry.entryEndUtcMillis, secondEntry.mIsBlocked)); + entries.set( + 0, + new TableEntry( + secondEntry.channelId, + secondEntry.program, + secondEntry.scheduledRecording, + mStartUtcMillis, + secondEntry.entryEndUtcMillis, + secondEntry.mIsBlocked)); } } return entries; @@ -662,8 +671,8 @@ public class ProgramManager { /** * Entry for program guide table. An "entry" can be either an actual program or a gap between - * programs. This is needed for {@link ProgramListAdapter} because - * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items. + * programs. This is needed for {@link ProgramListAdapter} because {@link + * android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items. */ static class TableEntry { /** Channel ID which this entry is included. */ @@ -686,18 +695,27 @@ public class ProgramManager { this(channelId, null, startUtcMillis, endUtcMillis, false); } - private TableEntry(long channelId, long startUtcMillis, long endUtcMillis, - boolean blocked) { + private TableEntry( + long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) { this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); } - private TableEntry(long channelId, Program program, long entryStartUtcMillis, - long entryEndUtcMillis, boolean isBlocked) { + private TableEntry( + long channelId, + Program program, + long entryStartUtcMillis, + long entryEndUtcMillis, + boolean isBlocked) { this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked); } - private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording, - long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) { + private TableEntry( + long channelId, + Program program, + ScheduledRecording scheduledRecording, + long entryStartUtcMillis, + long entryEndUtcMillis, + boolean isBlocked) { this.channelId = channelId; this.program = program; this.scheduledRecording = scheduledRecording; @@ -706,46 +724,34 @@ public class ProgramManager { mIsBlocked = isBlocked; } - /** - * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}. - */ + /** A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}. */ long getId() { // using a negative entryEndUtcMillis keeps it from conflicting with program Id return program != null ? program.getId() : -entryEndUtcMillis; } - /** - * Returns true if this is a gap. - */ + /** Returns true if this is a gap. */ boolean isGap() { - return !Program.isValid(program); + return !Program.isProgramValid(program); } - /** - * Returns true if this channel is blocked. - */ + /** Returns true if this channel is blocked. */ boolean isBlocked() { return mIsBlocked; } - /** - * Returns true if this program is on the air. - */ + /** Returns true if this program is on the air. */ boolean isCurrentProgram() { long current = System.currentTimeMillis(); return entryStartUtcMillis <= current && entryEndUtcMillis > current; } - /** - * Returns if this program has the genre. - */ + /** Returns if this program has the genre. */ boolean hasGenre(int genreId) { return !isGap() && program.hasGenre(genreId); } - /** - * Returns the width of table entry, in pixels. - */ + /** Returns the width of table entry, in pixels. */ int getWidth() { return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); } @@ -753,17 +759,42 @@ public class ProgramManager { @Override public String toString() { return "TableEntry{" - + "hashCode=" + hashCode() - + ", channelId=" + channelId - + ", program=" + program - + ", startTime=" + Utils.toTimeString(entryStartUtcMillis) - + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}"; - } + + "hashCode=" + + hashCode() + + ", channelId=" + + channelId + + ", program=" + + program + + ", startTime=" + + Utils.toTimeString(entryStartUtcMillis) + + ", endTimeTime=" + + Utils.toTimeString(entryEndUtcMillis) + + "}"; + } + } + + @VisibleForTesting + public static TableEntry createTableEntryForTest( + long channelId, + Program program, + ScheduledRecording scheduledRecording, + long entryStartUtcMillis, + long entryEndUtcMillis, + boolean isBlocked) { + return new TableEntry( + channelId, + program, + scheduledRecording, + entryStartUtcMillis, + entryEndUtcMillis, + isBlocked); } interface Listener { void onGenresUpdated(); + void onChannelsUpdated(); + void onTimeRangeUpdated(); } @@ -777,12 +808,12 @@ public class ProgramManager { static class ListenerAdapter implements Listener { @Override - public void onGenresUpdated() { } + public void onGenresUpdated() {} @Override - public void onChannelsUpdated() { } + public void onChannelsUpdated() {} @Override - public void onTimeRangeUpdated() { } + public void onTimeRangeUpdated() {} } } diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index fefc724c..83175bb6 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -24,12 +24,9 @@ import android.util.Log; import android.util.Range; import android.view.View; import android.view.ViewTreeObserver.OnGlobalLayoutListener; - -import com.android.tv.MainActivity; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.Utils; - import java.util.concurrent.TimeUnit; public class ProgramRow extends TimelineGridView { @@ -47,25 +44,23 @@ public class ProgramRow extends TimelineGridView { interface ChildFocusListener { /** - * Is called after focus is moved. Caller should check if old and new focuses are - * listener's children. - * See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}. + * Is called after focus is moved. Caller should check if old and new focuses are listener's + * children. See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}. */ void onChildFocus(View oldFocus, View newFocus); } - /** - * Used only for debugging. - */ + /** Used only for debugging. */ private Channel mChannel; - private final OnGlobalLayoutListener mLayoutListener = new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getViewTreeObserver().removeOnGlobalLayoutListener(this); - updateChildVisibleArea(); - } - }; + private final OnGlobalLayoutListener mLayoutListener = + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + updateChildVisibleArea(); + } + }; public ProgramRow(Context context) { this(context, null); @@ -79,9 +74,7 @@ public class ProgramRow extends TimelineGridView { super(context, attrs, defStyle); } - /** - * Registers a listener focus events occurring on children to the {@code ProgramRow}. - */ + /** Registers a listener focus events occurring on children to the {@code ProgramRow}. */ public void setChildFocusListener(ChildFocusListener childFocusListener) { mChildFocusListener = childFocusListener; } @@ -108,9 +101,7 @@ public class ProgramRow extends TimelineGridView { updateChildVisibleArea(); } - /** - * Moves focus to the current program. - */ + /** Moves focus to the current program. */ public void focusCurrentProgram() { View currentProgram = getCurrentProgramView(); if (currentProgram == null) { @@ -124,13 +115,15 @@ public class ProgramRow extends TimelineGridView { // Call this API after RTL is resolved. (i.e. View is measured.) private boolean isDirectionStart(int direction) { return getLayoutDirection() == LAYOUT_DIRECTION_LTR - ? direction == View.FOCUS_LEFT : direction == View.FOCUS_RIGHT; + ? direction == View.FOCUS_LEFT + : direction == View.FOCUS_RIGHT; } // Call this API after RTL is resolved. (i.e. View is measured.) private boolean isDirectionEnd(int direction) { return getLayoutDirection() == LAYOUT_DIRECTION_LTR - ? direction == View.FOCUS_RIGHT : direction == View.FOCUS_LEFT; + ? direction == View.FOCUS_RIGHT + : direction == View.FOCUS_LEFT; } @Override @@ -142,8 +135,8 @@ public class ProgramRow extends TimelineGridView { if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) { if (focusedEntry.entryStartUtcMillis < fromMillis) { // The current entry starts outside of the view; Align or scroll to the left. - scrollByTime(Math.max(-ONE_HOUR_MILLIS, - focusedEntry.entryStartUtcMillis - fromMillis)); + scrollByTime( + Math.max(-ONE_HOUR_MILLIS, focusedEntry.entryStartUtcMillis - fromMillis)); return focused; } } else if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) { @@ -169,17 +162,19 @@ public class ProgramRow extends TimelineGridView { TableEntry targetEntry = ((ProgramItemView) target).getTableEntry(); if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) { - if (targetEntry.entryStartUtcMillis < fromMillis && - targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) { + if (targetEntry.entryStartUtcMillis < fromMillis + && targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) { // The target entry starts outside the view; Align or scroll to the left. - scrollByTime(Math.max(-ONE_HOUR_MILLIS, - targetEntry.entryStartUtcMillis - fromMillis)); + scrollByTime( + Math.max(-ONE_HOUR_MILLIS, targetEntry.entryStartUtcMillis - fromMillis)); } } else if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) { if (targetEntry.entryStartUtcMillis > fromMillis + ONE_HOUR_MILLIS + HALF_HOUR_MILLIS) { // The target entry starts outside the view; Align or scroll to the right. - scrollByTime(Math.min(ONE_HOUR_MILLIS, - targetEntry.entryStartUtcMillis - fromMillis - ONE_HOUR_MILLIS)); + scrollByTime( + Math.min( + ONE_HOUR_MILLIS, + targetEntry.entryStartUtcMillis - fromMillis - ONE_HOUR_MILLIS)); } } @@ -188,8 +183,11 @@ public class ProgramRow extends TimelineGridView { private void scrollByTime(long timeToScroll) { if (DEBUG) { - Log.d(TAG, "scrollByTime(timeToScroll=" - + TimeUnit.MILLISECONDS.toMinutes(timeToScroll) + "min)"); + Log.d( + TAG, + "scrollByTime(timeToScroll=" + + TimeUnit.MILLISECONDS.toMinutes(timeToScroll) + + "min)"); } mProgramManager.shiftTime(timeToScroll); } @@ -203,12 +201,13 @@ public class ProgramRow extends TimelineGridView { // The focus is lost due to information loaded. Requests focus immediately. // (Because this entry is detached after real entries attached, we can't take // the below approach to resume focus on entry being attached.) - post(new Runnable() { - @Override - public void run() { - requestFocus(); - } - }); + post( + new Runnable() { + @Override + public void run() { + requestFocus(); + } + }); } else if (entry.isCurrentProgram()) { if (DEBUG) Log.d(TAG, "Keep focus to the current program"); // Current program is visible in the guide. @@ -227,12 +226,13 @@ public class ProgramRow extends TimelineGridView { TableEntry entry = ((ProgramItemView) child).getTableEntry(); if (entry.isCurrentProgram()) { mKeepFocusToCurrentProgram = false; - post(new Runnable() { - @Override - public void run() { - requestFocus(); - } - }); + post( + new Runnable() { + @Override + public void run() { + requestFocus(); + } + }); } } } @@ -243,8 +243,12 @@ public class ProgramRow extends TimelineGridView { // Give focus according to the previous focused range Range<Integer> focusRange = programGrid.getFocusRange(); - View nextFocus = GuideUtils.findNextFocusedProgram(this, focusRange.getLower(), - focusRange.getUpper(), programGrid.isKeepCurrentProgramFocused()); + View nextFocus = + GuideUtils.findNextFocusedProgram( + this, + focusRange.getLower(), + focusRange.getUpper(), + programGrid.isKeepCurrentProgramFocused()); if (nextFocus != null) { return nextFocus.requestFocus(); @@ -279,30 +283,29 @@ public class ProgramRow extends TimelineGridView { mChannel = channel; } - /** - * Sets the instance of {@link ProgramGuide} - */ + /** Sets the instance of {@link ProgramGuide} */ public void setProgramGuide(ProgramGuide programGuide) { mProgramGuide = programGuide; mProgramManager = programGuide.getProgramManager(); } - /** - * Resets the scroll with the initial offset {@code scrollOffset}. - */ + /** Resets the scroll with the initial offset {@code scrollOffset}. */ public void resetScroll(int scrollOffset) { - long startTime = GuideUtils.convertPixelToMillis(scrollOffset) - + mProgramManager.getStartTime(); - int position = mChannel == null ? -1 : mProgramManager.getProgramIndexAtTime( - mChannel.getId(), startTime); + long startTime = + GuideUtils.convertPixelToMillis(scrollOffset) + mProgramManager.getStartTime(); + int position = + mChannel == null + ? -1 + : mProgramManager.getProgramIndexAtTime(mChannel.getId(), startTime); if (position < 0) { getLayoutManager().scrollToPosition(0); } else { TableEntry entry = mProgramManager.getTableEntry(mChannel.getId(), position); - int offset = GuideUtils.convertMillisToPixel( - mProgramManager.getStartTime(), entry.entryStartUtcMillis) - scrollOffset; - ((LinearLayoutManager) getLayoutManager()) - .scrollToPositionWithOffset(position, offset); + int offset = + GuideUtils.convertMillisToPixel( + mProgramManager.getStartTime(), entry.entryStartUtcMillis) + - scrollOffset; + ((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, offset); // Workaround to b/31598505. When a program's duration is too long, // RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset(). // Therefore we have to update children's visible areas by ourselves in this case. diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index 99f853b1..6e7485ac 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -16,8 +16,6 @@ package com.android.tv.guide; -import static com.android.tv.util.ImageLoader.ImageLoaderCallback; - import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; @@ -49,32 +47,30 @@ import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeL import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.TvSingletons; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.Program; import com.android.tv.data.Program.CriticScore; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; -import com.android.tv.util.ImageCache; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import com.android.tv.util.images.ImageCache; +import com.android.tv.util.images.ImageLoader; +import com.android.tv.util.images.ImageLoader.ImageLoaderCallback; +import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask; import java.util.ArrayList; import java.util.List; -/** - * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. - */ +/** Adapts the {@link ProgramListAdapter} list to the body of the program guide table. */ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowViewHolder> implements ProgramManager.TableEntryChangedListener { private static final String TAG = "ProgramTableAdapter"; @@ -118,10 +114,10 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mContext = context; mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); - mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); + mTvInputManagerHelper = TvSingletons.getSingletons(context).getTvInputManagerHelper(); if (CommonFeatures.DVR.isEnabled(context)) { - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrManager = TvSingletons.getSingletons(context).getDvrManager(); + mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); } else { mDvrManager = null; mDvrDataManager = null; @@ -130,58 +126,62 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mProgramManager = programGuide.getProgramManager(); Resources res = context.getResources(); - mChannelLogoWidth = res.getDimensionPixelSize( - R.dimen.program_guide_table_header_column_channel_logo_width); - mChannelLogoHeight = res.getDimensionPixelSize( - R.dimen.program_guide_table_header_column_channel_logo_height); - mImageWidth = res.getDimensionPixelSize( - R.dimen.program_guide_table_detail_image_width); - mImageHeight = res.getDimensionPixelSize( - R.dimen.program_guide_table_detail_image_height); - mProgramTitleForNoInformation = res.getString( - R.string.program_title_for_no_information); - mProgramTitleForBlockedChannel = res.getString( - R.string.program_title_for_blocked_channel); - mChannelTextColor = res.getColor( - R.color.program_guide_table_header_column_channel_number_text_color, null); - mChannelBlockedTextColor = res.getColor( - R.color.program_guide_table_header_column_channel_number_blocked_text_color, null); - mDetailTextColor = res.getColor( - R.color.program_guide_table_detail_title_text_color, null); - mDetailGrayedTextColor = res.getColor( - R.color.program_guide_table_detail_title_grayed_text_color, null); + mChannelLogoWidth = + res.getDimensionPixelSize( + R.dimen.program_guide_table_header_column_channel_logo_width); + mChannelLogoHeight = + res.getDimensionPixelSize( + R.dimen.program_guide_table_header_column_channel_logo_height); + mImageWidth = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_image_width); + mImageHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_image_height); + mProgramTitleForNoInformation = res.getString(R.string.program_title_for_no_information); + mProgramTitleForBlockedChannel = res.getString(R.string.program_title_for_blocked_channel); + mChannelTextColor = + res.getColor( + R.color.program_guide_table_header_column_channel_number_text_color, null); + mChannelBlockedTextColor = + res.getColor( + R.color.program_guide_table_header_column_channel_number_blocked_text_color, + null); + mDetailTextColor = res.getColor(R.color.program_guide_table_detail_title_text_color, null); + mDetailGrayedTextColor = + res.getColor(R.color.program_guide_table_detail_title_grayed_text_color, null); mAnimationDuration = res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); - mDetailPadding = res.getDimensionPixelOffset( - R.dimen.program_guide_table_detail_padding); + mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding); mProgramRecordableText = res.getString(R.string.dvr_epg_program_recordable); mRecordingScheduledText = res.getString(R.string.dvr_epg_program_recording_scheduled); mRecordingConflictText = res.getString(R.string.dvr_epg_program_recording_conflict); mRecordingFailedText = res.getString(R.string.dvr_epg_program_recording_failed); mRecordingInProgressText = res.getString(R.string.dvr_epg_program_recording_in_progress); - mDvrPaddingStartWithTrack = res.getDimensionPixelOffset( - R.dimen.program_guide_table_detail_dvr_margin_start); - mDvrPaddingStartWithOutTrack = res.getDimensionPixelOffset( - R.dimen.program_guide_table_detail_dvr_margin_start_without_track); - - int episodeTitleSize = res.getDimensionPixelSize( - R.dimen.program_guide_table_detail_episode_title_text_size); - ColorStateList episodeTitleColor = ColorStateList.valueOf( - res.getColor(R.color.program_guide_table_detail_episode_title_text_color, null)); - mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, - episodeTitleColor, null); + mDvrPaddingStartWithTrack = + res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_dvr_margin_start); + mDvrPaddingStartWithOutTrack = + res.getDimensionPixelOffset( + R.dimen.program_guide_table_detail_dvr_margin_start_without_track); + + int episodeTitleSize = + res.getDimensionPixelSize( + R.dimen.program_guide_table_detail_episode_title_text_size); + ColorStateList episodeTitleColor = + ColorStateList.valueOf( + res.getColor( + R.color.program_guide_table_detail_episode_title_text_color, null)); + mEpisodeTitleStyle = + new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, null); mCriticScoreViews = new ArrayList<>(); mRecycledViewPool = new RecycledViewPool(); - mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item, - context.getResources().getInteger( - R.integer.max_recycled_view_pool_epg_table_item)); - mProgramManager.addListener(new ProgramManager.ListenerAdapter() { - @Override - public void onChannelsUpdated() { - update(); - } - }); + mRecycledViewPool.setMaxRecycledViews( + R.layout.program_guide_table_item, + context.getResources().getInteger(R.integer.max_recycled_view_pool_epg_table_item)); + mProgramManager.addListener( + new ProgramManager.ListenerAdapter() { + @Override + public void onChannelsUpdated() { + update(); + } + }); update(); mProgramManager.addTableEntryChangedListener(this); } @@ -193,8 +193,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr } mProgramListAdapters.clear(); for (int i = 0; i < mProgramManager.getChannelCount(); i++) { - ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), - mProgramGuide, i); + ProgramListAdapter listAdapter = + new ProgramListAdapter(mContext.getResources(), mProgramGuide, i); mProgramManager.addTableEntriesUpdatedListener(listAdapter); mProgramListAdapters.add(listAdapter); } @@ -250,29 +250,31 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr private ProgramManager.TableEntry mSelectedEntry; private Animator mDetailOutAnimator; private Animator mDetailInAnimator; - private final Runnable mDetailInStarter = new Runnable() { - @Override - public void run() { - mProgramRow.removeOnScrollListener(mOnScrollListener); - if (mDetailInAnimator != null) { - mDetailInAnimator.start(); - } - } - }; - private final Runnable mUpdateDetailViewRunnable = new Runnable() { - @Override - public void run() { - updateDetailView(); - } - }; + private final Runnable mDetailInStarter = + new Runnable() { + @Override + public void run() { + mProgramRow.removeOnScrollListener(mOnScrollListener); + if (mDetailInAnimator != null) { + mDetailInAnimator.start(); + } + } + }; + private final Runnable mUpdateDetailViewRunnable = + new Runnable() { + @Override + public void run() { + updateDetailView(); + } + }; private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - onHorizontalScrolled(); - } - }; + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + onHorizontalScrolled(); + } + }; private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = new ViewTreeObserver.OnGlobalFocusChangeListener() { @@ -313,8 +315,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr new AccessibilityManager.AccessibilityStateChangeListener() { @Override public void onAccessibilityStateChanged(boolean enable) { - enable &= !TvCommonUtils.isRunningInTest(); - mDetailView.setFocusable(enable); + enable &= !CommonUtils.isRunningInTest(); mChannelHeaderView.setFocusable(enable); } }; @@ -327,7 +328,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { - mContainer.getViewTreeObserver() + mContainer + .getViewTreeObserver() .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); mAccessibilityManager.addAccessibilityStateChangeListener( mAccessibilityStateChangeListener); @@ -335,7 +337,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr @Override public void onViewDetachedFromWindow(View v) { - mContainer.getViewTreeObserver() + mContainer + .getViewTreeObserver() .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); mAccessibilityManager.removeAccessibilityStateChangeListener( mAccessibilityStateChangeListener); @@ -364,9 +367,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); - boolean accessibilityEnabled = mAccessibilityManager.isEnabled() - && !TvCommonUtils.isRunningInTest(); - mDetailView.setFocusable(accessibilityEnabled); + boolean accessibilityEnabled = + mAccessibilityManager.isEnabled() && !CommonUtils.isRunningInTest(); mChannelHeaderView.setFocusable(accessibilityEnabled); } @@ -382,9 +384,10 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mDetailView.setVisibility(View.GONE); // The bottom-left of the last channel header view will have a rounded corner. - mChannelHeaderView.setBackgroundResource((position < mProgramListAdapters.size() - 1) - ? R.drawable.program_guide_table_header_column_item_background - : R.drawable.program_guide_table_header_column_last_item_background); + mChannelHeaderView.setBackgroundResource( + (position < mProgramListAdapters.size() - 1) + ? R.drawable.program_guide_table_header_column_item_background + : R.drawable.program_guide_table_header_column_last_item_background); } private void onBindChannel(Channel channel) { @@ -411,7 +414,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr } else { size = R.dimen.program_guide_table_header_column_channel_number_small_font_size; } - mChannelNumberView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + mChannelNumberView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, mChannelNumberView.getContext().getResources().getDimension(size)); mChannelNumberView.setText(displayNumber); mChannelNumberView.setVisibility(View.VISIBLE); @@ -429,8 +433,11 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mChannelNameView.setVisibility(View.VISIBLE); mChannelBlockView.setVisibility(View.GONE); - mChannel.loadBitmap(itemView.getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, - mChannelLogoWidth, mChannelLogoHeight, + mChannel.loadBitmap( + itemView.getContext(), + Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mChannelLogoWidth, + mChannelLogoHeight, createChannelLogoLoadedCallback(this, channel.getId())); } } @@ -439,7 +446,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr public void onChildFocus(View oldFocus, View newFocus) { if (newFocus == null) { return; - } // When the accessibility service is enabled, focus might be put on channel's header or + } // When the accessibility service is enabled, focus might be put on channel's header + // or // detail view, besides program items. if (newFocus == mChannelHeaderView) { mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry(); @@ -461,7 +469,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr return; } - if (Program.isValid(mSelectedEntry.program)) { + if (Program.isProgramValid(mSelectedEntry.program)) { Program program = mSelectedEntry.program; if (getProgramBlock(program) == null) { program.prefetchPosterArt(itemView.getContext(), mImageWidth, mImageHeight); @@ -473,10 +481,12 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr View detailContentView = mDetailView.findViewById(R.id.detail_content); if (mDetailInAnimator == null) { - mDetailOutAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, - PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), - PropertyValuesHolder.ofFloat(View.TRANSLATION_X, - 0f, direction * mDetailPadding)); + mDetailOutAnimator = + ObjectAnimator.ofPropertyValuesHolder( + detailContentView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), + PropertyValuesHolder.ofFloat( + View.TRANSLATION_X, 0f, direction * mDetailPadding)); mDetailOutAnimator.setDuration(mAnimationDuration); mDetailOutAnimator.addListener( new HardwareLayerAnimatorListenerAdapter(detailContentView) { @@ -501,10 +511,12 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mHandler.postDelayed(mDetailInStarter, mAnimationDuration); } - mDetailInAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, - PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), - PropertyValuesHolder.ofFloat(View.TRANSLATION_X, - direction * -mDetailPadding, 0f)); + mDetailInAnimator = + ObjectAnimator.ofPropertyValuesHolder( + detailContentView, + PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), + PropertyValuesHolder.ofFloat( + View.TRANSLATION_X, direction * -mDetailPadding, 0f)); mDetailInAnimator.setDuration(mAnimationDuration); mDetailInAnimator.addListener( new HardwareLayerAnimatorListenerAdapter(detailContentView) { @@ -529,7 +541,7 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr } if (DEBUG) Log.d(TAG, "updateDetailView"); mCriticScoresLayout.removeAllViews(); - if (Program.isValid(mSelectedEntry.program)) { + if (Program.isProgramValid(mSelectedEntry.program)) { mTitleView.setTextColor(mDetailTextColor); Context context = itemView.getContext(); Program program = mSelectedEntry.program; @@ -538,7 +550,10 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr updatePosterArt(null); if (blockedRating == null) { - program.loadPosterArt(context, mImageWidth, mImageHeight, + program.loadPosterArt( + context, + mImageWidth, + mImageHeight, createProgramPosterArtCallback(this, program)); } @@ -550,24 +565,35 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr String fullTitle = title + " " + episodeTitle; SpannableString text = new SpannableString(fullTitle); - text.setSpan(mEpisodeTitleStyle, - fullTitle.length() - episodeTitle.length(), fullTitle.length(), + text.setSpan( + mEpisodeTitleStyle, + fullTitle.length() - episodeTitle.length(), + fullTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mTitleView.setText(text); } - updateTextView(mTimeView, Utils.getDurationString(context, - program.getStartTimeUtcMillis(), - program.getEndTimeUtcMillis(), false)); - - boolean trackMetaDataVisible = updateTextView(mAspectRatioView, Utils - .getAspectRatioString(program.getVideoWidth(), program.getVideoHeight())); - - int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize( - program.getVideoWidth(), program.getVideoHeight()); + updateTextView( + mTimeView, + Utils.getDurationString( + context, + program.getStartTimeUtcMillis(), + program.getEndTimeUtcMillis(), + false)); + + boolean trackMetaDataVisible = + updateTextView( + mAspectRatioView, + Utils.getAspectRatioString( + program.getVideoWidth(), program.getVideoHeight())); + + int videoDefinitionLevel = + Utils.getVideoDefinitionLevelFromSize( + program.getVideoWidth(), program.getVideoHeight()); trackMetaDataVisible |= - updateTextView(mResolutionView, Utils.getVideoDefinitionLevelString( - context, videoDefinitionLevel)); + updateTextView( + mResolutionView, + Utils.getVideoDefinitionLevelString(context, videoDefinitionLevel)); if (mDvrManager != null && mDvrManager.isProgramRecordable(program)) { ScheduledRecording scheduledRecording = @@ -616,7 +642,6 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mDvrIndicator.setVisibility(View.GONE); } - if (blockedRating == null) { mBlockView.setVisibility(View.GONE); updateTextView(mDescriptionView, program.getDescription()); @@ -655,8 +680,10 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr } private String getBlockedDescription(TvContentRating blockedRating) { - String name = mTvInputManagerHelper.getContentRatingsManager() - .getDisplayNameForRating(blockedRating); + String name = + mTvInputManagerHelper + .getContentRatingsManager() + .getDisplayNameForRating(blockedRating); if (TextUtils.isEmpty(name)) { return mContext.getString(R.string.program_guide_content_locked); } else { @@ -691,8 +718,9 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mIsInputLogoVisible = true; TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId()); if (info != null) { - LoadTvInputLogoTask task = new LoadTvInputLogoTask( - itemView.getContext(), ImageCache.getInstance(), info); + LoadTvInputLogoTask task = + new LoadTvInputLogoTask( + itemView.getContext(), ImageCache.getInstance(), info); ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task); } } @@ -735,23 +763,28 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr mInputLogoView.setVisibility(View.VISIBLE); } - private void updateCriticScoreView(ProgramRowViewHolder holder, final long programId, - CriticScore criticScore, View view) { + private void updateCriticScoreView( + ProgramRowViewHolder holder, + final long programId, + CriticScore criticScore, + View view) { TextView criticScoreSource = (TextView) view.findViewById(R.id.critic_score_source); TextView criticScoreText = (TextView) view.findViewById(R.id.critic_score_score); ImageView criticScoreLogo = (ImageView) view.findViewById(R.id.critic_score_logo); - //set the appropriate information in the views + // set the appropriate information in the views if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - criticScoreSource.setText(Html.fromHtml(criticScore.source, - Html.FROM_HTML_MODE_LEGACY)); + criticScoreSource.setText( + Html.fromHtml(criticScore.source, Html.FROM_HTML_MODE_LEGACY)); } else { criticScoreSource.setText(Html.fromHtml(criticScore.source)); } criticScoreText.setText(criticScore.score); criticScoreSource.setVisibility(View.VISIBLE); criticScoreText.setVisibility(View.VISIBLE); - ImageLoader.loadBitmap(mContext, criticScore.logoUrl, + ImageLoader.loadBitmap( + mContext, + criticScore.logoUrl, createCriticScoreLogoCallback(holder, programId, criticScoreLogo)); } @@ -768,7 +801,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logoImage) { - if (logoImage == null || holder.mSelectedEntry == null + if (logoImage == null + || holder.mSelectedEntry == null || holder.mSelectedEntry.program == null || holder.mSelectedEntry.program.getId() != programId) { logoView.setVisibility(View.GONE); @@ -785,7 +819,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap posterArt) { - if (posterArt == null || holder.mSelectedEntry == null + if (posterArt == null + || holder.mSelectedEntry == null || holder.mSelectedEntry.program == null) { return; } @@ -803,7 +838,8 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { - if (logo == null || holder.mChannel == null + if (logo == null + || holder.mChannel == null || holder.mChannel.getId() != channelId) { return; } @@ -817,8 +853,9 @@ class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.Progr return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { - if (logo != null && holder.mChannel != null && info.getId() - .equals(holder.mChannel.getInputId())) { + if (logo != null + && holder.mChannel != null + && info.getId().equals(holder.mChannel.getInputId())) { holder.updateInputLogoInternal(logo); } } diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java index d9e96a40..9c10c952 100644 --- a/src/com/android/tv/guide/TimeListAdapter.java +++ b/src/com/android/tv/guide/TimeListAdapter.java @@ -16,7 +16,6 @@ package com.android.tv.guide; -import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.text.format.DateFormat; @@ -24,17 +23,15 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.util.Utils; - import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; /** - * Adapts the time range from {@link ProgramManager} to the timeline header row of the program - * guide table. + * Adapts the time range from {@link ProgramManager} to the timeline header row of the program guide + * table. */ class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> { private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30); @@ -53,8 +50,10 @@ class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolde TimeListAdapter(Resources res) { if (sRowHeaderOverlapping == 0) { - sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset( - R.dimen.program_guide_table_header_row_overlap)); + sRowHeaderOverlapping = + Math.abs( + res.getDimensionPixelOffset( + R.dimen.program_guide_table_header_row_overlap)); } Locale locale = res.getConfiguration().locale; mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY); diff --git a/src/com/android/tv/guide/TimelineGridView.java b/src/com/android/tv/guide/TimelineGridView.java index 8af1c8a3..c4922b75 100644 --- a/src/com/android/tv/guide/TimelineGridView.java +++ b/src/com/android/tv/guide/TimelineGridView.java @@ -34,14 +34,15 @@ public class TimelineGridView extends RecyclerView { public TimelineGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) { - @Override - public boolean onRequestChildFocus(RecyclerView parent, State state, View child, - View focused) { - // This disables the default scroll behavior for focus movement. - return true; - } - }); + setLayoutManager( + new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) { + @Override + public boolean onRequestChildFocus( + RecyclerView parent, State state, View child, View focused) { + // This disables the default scroll behavior for focus movement. + return true; + } + }); // RecyclerView is always focusable, however this is not desirable for us, so disable. // See b/18863217 (ag/634046) for reasons to why RecyclerView is focusable. diff --git a/src/com/android/tv/guide/TimelineRow.java b/src/com/android/tv/guide/TimelineRow.java index 3f0c8678..b6a10ab1 100644 --- a/src/com/android/tv/guide/TimelineRow.java +++ b/src/com/android/tv/guide/TimelineRow.java @@ -40,19 +40,16 @@ public class TimelineRow extends TimelineGridView { getLayoutManager().scrollToPosition(0); } - /** - * Returns the current scroll position - */ + /** Returns the current scroll position */ public int getScrollOffset() { return Math.abs(mScrollPosition); } - /** - * Scrolls horizontally to the given position. - */ + /** Scrolls horizontally to the given position. */ public void scrollTo(int scrollOffset, boolean smoothScroll) { - int dx = (scrollOffset - getScrollOffset()) - * (getLayoutDirection() == LAYOUT_DIRECTION_LTR ? 1 : -1); + int dx = + (scrollOffset - getScrollOffset()) + * (getLayoutDirection() == LAYOUT_DIRECTION_LTR ? 1 : -1); if (smoothScroll) { smoothScrollBy(dx, 0); } else { diff --git a/src/com/android/tv/license/LicenseDialogFragment.java b/src/com/android/tv/license/LicenseDialogFragment.java index b0e09776..74b99dcd 100644 --- a/src/com/android/tv/license/LicenseDialogFragment.java +++ b/src/com/android/tv/license/LicenseDialogFragment.java @@ -26,7 +26,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.dialog.SafeDismissDialogFragment; diff --git a/src/com/android/tv/license/LicenseSideFragment.java b/src/com/android/tv/license/LicenseSideFragment.java index fd92467c..ff99df2a 100644 --- a/src/com/android/tv/license/LicenseSideFragment.java +++ b/src/com/android/tv/license/LicenseSideFragment.java @@ -17,11 +17,9 @@ package com.android.tv.license; import android.content.Context; - import com.android.tv.R; import com.android.tv.ui.sidepanel.ActionItem; import com.android.tv.ui.sidepanel.SideFragment; - import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/license/LicenseUtils.java b/src/com/android/tv/license/LicenseUtils.java index cf3fe751..1bae0c6a 100644 --- a/src/com/android/tv/license/LicenseUtils.java +++ b/src/com/android/tv/license/LicenseUtils.java @@ -17,22 +17,14 @@ package com.android.tv.license; import android.content.res.AssetManager; - -import java.io.File; import java.io.IOException; import java.io.InputStream; -/** - * Utilities for showing open source licenses. - */ +/** Utilities for showing open source licenses. */ public final class LicenseUtils { - public final static String RATING_SOURCE_FILE = - "file:///android_asset/rating_sources.html"; + public static final String RATING_SOURCE_FILE = "file:///android_asset/rating_sources.html"; - - /** - * Checks if the rating_attribution.html asset is include in the apk. - */ + /** Checks if the rating_attribution.html asset is include in the apk. */ public static boolean hasRatingAttribution(AssetManager am) { try (InputStream is = am.open("rating_sources.html")) { return true; @@ -41,6 +33,5 @@ public final class LicenseUtils { } } - private LicenseUtils() { - } + private LicenseUtils() {} } diff --git a/src/com/android/tv/license/Licenses.java b/src/com/android/tv/license/Licenses.java index 4b8a7ffc..1da91d7a 100644 --- a/src/com/android/tv/license/Licenses.java +++ b/src/com/android/tv/license/Licenses.java @@ -18,10 +18,8 @@ package com.android.tv.license; import android.content.Context; import android.support.annotation.RawRes; - import com.android.tv.R; import com.android.tv.common.SoftPreconditions; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -36,6 +34,7 @@ import java.util.Collections; public final class Licenses { public static final String TAG = "Licenses"; + public static boolean hasLicenses(Context context) { return !getTextFromResource( context.getApplicationContext(), R.raw.third_party_license_metadata, 0, -1) diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl b/src/com/android/tv/livetv/receiver/GlobalKeyReceiver.java index ed053790..80ea72f4 100644 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl +++ b/src/com/android/tv/livetv/receiver/GlobalKeyReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,14 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ffmpeg; +package com.android.tv.livetv.receiver; -interface IFfmpegDecoder { - boolean isAvailable(); - void create(); - void release(); - void resetDecoderState(String mimetype); - void decode(long timeUs, in byte[] sample); - byte[] getDecodedSample(); - long getDecodedTimeUs(); - void testSandboxIsolatedProcess(); - void testSandboxMinijail(); -}
\ No newline at end of file +import com.android.tv.receiver.AbstractGlobalKeyReceiver; + +/** + * Global Key Receiver for LiveTv. + * + * <p>This is an exported receiver and the name can not change. + * Partners that modify LiveTv source should rename this class. + */ +public final class GlobalKeyReceiver extends AbstractGlobalKeyReceiver {} diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java index 2fd70bfb..3ecd5f5c 100644 --- a/src/com/android/tv/menu/ActionCardView.java +++ b/src/com/android/tv/menu/ActionCardView.java @@ -22,12 +22,9 @@ import android.util.Log; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; - import com.android.tv.R; -/** - * A view to render an item of TV options. - */ +/** A view to render an item of TV options. */ public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView<MenuAction> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; @@ -97,5 +94,5 @@ public class ActionCardView extends RelativeLayout implements ItemListRowView.Ca } @Override - public void onRecycled() { } + public void onRecycled() {} } diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index 94ccd37f..fd93c314 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -33,19 +33,15 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; -import com.android.tv.util.BitmapUtils; -import com.android.tv.util.ImageLoader; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvInputManagerHelper; - +import com.android.tv.util.images.BitmapUtils; +import com.android.tv.util.images.ImageLoader; import java.util.Objects; -/** - * A view to render an app link card. - */ +/** A view to render an app link card. */ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; @@ -88,9 +84,7 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { mDefaultDrawable = getResources().getDrawable(R.drawable.ic_recent_thumbnail_default, null); } - /** - * Returns the intent which will be started once this card is clicked. - */ + /** Returns the intent which will be started once this card is clicked. */ public Intent getIntent() { return mIntent; } @@ -100,13 +94,20 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { Channel newChannel = item.getChannel(); boolean channelChanged = !Objects.equals(mChannel, newChannel); String previousPosterArtUri = mChannel == null ? null : mChannel.getAppLinkPosterArtUri(); - boolean posterArtChanged = previousPosterArtUri == null - || newChannel.getAppLinkPosterArtUri() == null - || !TextUtils.equals(previousPosterArtUri, newChannel.getAppLinkPosterArtUri()); + boolean posterArtChanged = + previousPosterArtUri == null + || newChannel.getAppLinkPosterArtUri() == null + || !TextUtils.equals( + previousPosterArtUri, newChannel.getAppLinkPosterArtUri()); mChannel = newChannel; if (DEBUG) { - Log.d(TAG, "onBind(channelName=" + mChannel.getDisplayName() + ", selected=" + selected - + ")"); + Log.d( + TAG, + "onBind(channelName=" + + mChannel.getDisplayName() + + ", selected=" + + selected + + ")"); } ApplicationInfo appInfo = mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId()); if (channelChanged) { @@ -120,8 +121,8 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { mAppInfoView.setVisibility(VISIBLE); mAppInfoView.setCompoundDrawablePadding(mIconPadding); mAppInfoView.setCompoundDrawablesRelative(null, null, null, null); - appLabel = mTvInputManagerHelper - .getTvInputApplicationLabel(mChannel.getInputId()); + appLabel = + mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId()); if (appLabel != null) { mAppInfoView.setText(appLabel); } else { @@ -140,7 +141,7 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { protected void onPostExecute(CharSequence appLabel) { mTvInputManagerHelper.setTvInputApplicationLabel( mLoadTvInputId, appLabel); - if (mLoadTvInputId != mChannel.getInputId() + if (mLoadTvInputId.equals(mChannel.getInputId()) || !isAttachedToWindow()) { return; } @@ -149,12 +150,17 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) { - mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON, - mIconWidth, mIconHeight, createChannelLogoCallback( + mChannel.loadBitmap( + getContext(), + Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON, + mIconWidth, + mIconHeight, + createChannelLogoCallback( this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON)); } else if (appInfo.icon != 0) { - Drawable appIcon = mTvInputManagerHelper - .getTvInputApplicationIcon(mChannel.getInputId()); + Drawable appIcon = + mTvInputManagerHelper.getTvInputApplicationIcon( + mChannel.getInputId()); if (appIcon != null) { BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon); appIcon.setBounds(0, 0, mIconWidth, mIconHeight); @@ -186,11 +192,14 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { } break; case Channel.APP_LINK_TYPE_APP: - appLabel = mTvInputManagerHelper - .getTvInputApplicationLabel(mChannel.getInputId()); + appLabel = + mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId()); if (appLabel != null) { - setText(getContext() - .getString(R.string.channels_item_app_link_app_launcher, appLabel)); + setText( + getContext() + .getString( + R.string.channels_item_app_link_app_launcher, + appLabel)); } else { new AsyncTask<Void, Void, CharSequence>() { private final String mLoadTvInputId = mChannel.getInputId(); @@ -206,15 +215,17 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { @Override protected void onPostExecute(CharSequence appLabel) { mTvInputManagerHelper.setTvInputApplicationLabel( - mLoadTvInputId, appLabel); + mLoadTvInputId, appLabel); if (!mLoadTvInputId.equals(mChannel.getInputId()) - || !isAttachedToWindow()) { + || !isAttachedToWindow()) { return; } - setText(getContext() - .getString( - R.string.channels_item_app_link_app_launcher, - appLabel)); + setText( + getContext() + .getString( + R.string + .channels_item_app_link_app_launcher, + appLabel)); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -235,9 +246,13 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { mImageView.setImageDrawable(mDefaultDrawable); mImageView.setForeground(null); if (!TextUtils.isEmpty(mChannel.getAppLinkPosterArtUri())) { - mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART, - mCardImageWidth, mCardImageHeight, createChannelLogoCallback(this, mChannel, - Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART)); + mChannel.loadBitmap( + getContext(), + Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART, + mCardImageWidth, + mCardImageHeight, + createChannelLogoCallback( + this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART)); } else { setCardImageWithBanner(appInfo); } @@ -265,10 +280,12 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { if (bitmap != null) { drawable = new BitmapDrawable(getResources(), bitmap); if (bitmap.getWidth() > bitmap.getHeight()) { - drawable.setBounds(0, 0, mIconWidth, - mIconWidth * bitmap.getHeight() / bitmap.getWidth()); + drawable.setBounds( + 0, 0, mIconWidth, mIconWidth * bitmap.getHeight() / bitmap.getWidth()); } else { - drawable.setBounds(0, 0, + drawable.setBounds( + 0, + 0, mIconHeight * bitmap.getWidth() / bitmap.getHeight(), mIconHeight); } @@ -303,6 +320,7 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { private void setCardImageWithBanner(ApplicationInfo appInfo) { new AsyncTask<Void, Void, Drawable>() { private String mLoadTvInputId = mChannel.getInputId(); + @Override protected Drawable doInBackground(Void... params) { Drawable banner = null; @@ -321,7 +339,7 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { @Override protected void onPostExecute(Drawable banner) { - if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) { + if (mLoadTvInputId.equals(mChannel.getInputId()) || !isAttachedToWindow()) { return; } if (banner != null) { @@ -341,6 +359,7 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { } else { new AsyncTask<Void, Void, Drawable>() { private final String mLoadTvInputId = mChannel.getInputId(); + @Override protected Drawable doInBackground(Void... params) { Drawable banner = null; @@ -373,8 +392,8 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { mImageView.setImageDrawable(mDefaultDrawable); mImageView.setBackgroundResource(R.color.channel_card); } else { - Bitmap bitmap = Bitmap.createBitmap( - mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); + Bitmap bitmap = + Bitmap.createBitmap(mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight); banner.draw(canvas); @@ -387,12 +406,19 @@ public class AppLinkCardView extends BaseCardView<ChannelsRowItem> { } private void extractAndSetMetaViewBackgroundColor(Bitmap bitmap) { - new Palette.Builder(bitmap).generate(new Palette.PaletteAsyncListener() { - @Override - public void onGenerated(Palette palette) { - mMetaViewHolder.setBackgroundColor(palette.getDarkVibrantColor( - getResources().getColor(R.color.channel_card_meta_background, null))); - } - }); + new Palette.Builder(bitmap) + .generate( + new Palette.PaletteAsyncListener() { + @Override + public void onGenerated(Palette palette) { + mMetaViewHolder.setBackgroundColor( + palette.getDarkVibrantColor( + getResources() + .getColor( + R.color + .channel_card_meta_background, + null))); + } + }); } } diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java index 4c5e6c78..3a94ebbf 100644 --- a/src/com/android/tv/menu/BaseCardView.java +++ b/src/com/android/tv/menu/BaseCardView.java @@ -29,12 +29,9 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.R; -/** - * A base class to render a card. - */ +/** A base class to render a card. */ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> { private static final float SCALE_FACTOR_0F = 0f; private static final float SCALE_FACTOR_1F = 1f; @@ -49,10 +46,8 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo private final float mExtendedCardHeight; private final float mTextViewHeight; private final float mExtendedTextViewHeight; - @Nullable - private TextView mTextView; - @Nullable - private TextView mTextViewFocused; + @Nullable private TextView mTextView; + @Nullable private TextView mTextViewFocused; private final int mCardImageWidth; private final float mCardHeight; private boolean mSelected; @@ -74,27 +69,32 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo setClipToOutline(true); mFocusAnimDuration = getResources().getInteger(R.integer.menu_focus_anim_duration); - mFocusTranslationZ = getResources().getDimension(R.dimen.channel_card_elevation_focused) - - getResources().getDimension(R.dimen.card_elevation_normal); - mVerticalCardMargin = 2 * ( - getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top) - + getResources().getDimensionPixelOffset(R.dimen.menu_list_margin_top)); + mFocusTranslationZ = + getResources().getDimension(R.dimen.channel_card_elevation_focused) + - getResources().getDimension(R.dimen.card_elevation_normal); + mVerticalCardMargin = + 2 + * (getResources().getDimensionPixelOffset(R.dimen.menu_list_padding_top) + + getResources() + .getDimensionPixelOffset(R.dimen.menu_list_margin_top)); // Ensure the same elevation and focus animation for all subclasses. setElevation(getResources().getDimension(R.dimen.card_elevation_normal)); mCardCornerRadius = getResources().getDimensionPixelSize(R.dimen.channel_card_round_radius); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius); - } - }); + setOutlineProvider( + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect( + 0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius); + } + }); mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height); - mExtendedCardHeight = getResources().getDimensionPixelSize( - R.dimen.card_layout_height_extended); + mExtendedCardHeight = + getResources().getDimensionPixelSize(R.dimen.card_layout_height_extended); mTextViewHeight = getResources().getDimensionPixelSize(R.dimen.card_meta_layout_height); - mExtendedTextViewHeight = getResources().getDimensionPixelOffset( - R.dimen.card_meta_layout_height_extended); + mExtendedTextViewHeight = + getResources().getDimensionPixelOffset(R.dimen.card_meta_layout_height_extended); } @Override @@ -104,16 +104,14 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo mTextViewFocused = (TextView) findViewById(R.id.card_text_focused); } - /** - * Called when the view is displayed. - */ + /** Called when the view is displayed. */ @Override public void onBind(T item, boolean selected) { setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } @Override - public void onRecycled() { } + public void onRecycled() {} @Override public void onSelected() { @@ -137,9 +135,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } } - /** - * Sets text of this card view. - */ + /** Sets text of this card view. */ public void setText(int resId) { if (mTextResId != resId) { mTextResId = resId; @@ -155,9 +151,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } } - /** - * Sets text of this card view. - */ + /** Sets text of this card view. */ public void setText(String text) { if (!TextUtils.equals(text, mTextString)) { mTextString = text; @@ -176,8 +170,8 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo private void onTextViewUpdated() { if (mTextView != null && mTextViewFocused != null) { mTextViewFocused.measure( - MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; if (mExtendViewOnFocus) { setTextViewFocusedAlpha(mSelected ? 1f : 0f); @@ -188,9 +182,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo setFocusAnimatedValue(mSelected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } - /** - * Enables or disables text view of this card view. - */ + /** Enables or disables text view of this card view. */ public void setTextViewEnabled(boolean enabled) { if (mTextViewFocused != null) { mTextViewFocused.setEnabled(enabled); @@ -200,38 +192,38 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } } - /** - * Called when the focus animation started. - */ + /** Called when the focus animation started. */ protected void onFocusAnimationStart(boolean selected) { if (mExtendViewOnFocus) { setTextViewFocusedAlpha(selected ? 1f : 0f); } } - /** - * Called when the focus animation ended. - */ + /** Called when the focus animation ended. */ protected void onFocusAnimationEnd(boolean selected) { // do nothing. } /** - * Called when the view is bound, or while focus animation is running with a value - * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}. + * Called when the view is bound, or while focus animation is running with a value between + * {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}. */ protected void onSetFocusAnimatedValue(float animatedValue) { - float cardViewHeight = (mExtendViewOnFocus && isFocused()) - ? mExtendedCardHeight : mCardHeight; + float cardViewHeight = + (mExtendViewOnFocus && isFocused()) ? mExtendedCardHeight : mCardHeight; float scale = 1f + (mVerticalCardMargin / cardViewHeight) * animatedValue; setScaleX(scale); setScaleY(scale); setTranslationZ(mFocusTranslationZ * animatedValue); if (mTextView != null && mTextViewFocused != null) { ViewGroup.LayoutParams params = mTextView.getLayoutParams(); - int height = mExtendViewOnFocus ? Math.round(mTextViewHeight - + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue) - : (int) mTextViewHeight; + int height = + mExtendViewOnFocus + ? Math.round( + mTextViewHeight + + (mExtendedTextViewHeight - mTextViewHeight) + * animatedValue) + : (int) mTextViewHeight; if (height != params.height) { params.height = height; setTextViewLayoutParams(params); @@ -252,25 +244,27 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo final boolean selected = targetAnimatedValue == SCALE_FACTOR_1F; mFocusAnimator = ValueAnimator.ofFloat(mFocusAnimatedValue, targetAnimatedValue); mFocusAnimator.setDuration(mFocusAnimDuration); - mFocusAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setHasTransientState(true); - onFocusAnimationStart(selected); - } + mFocusAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setHasTransientState(true); + onFocusAnimationStart(selected); + } - @Override - public void onAnimationEnd(Animator animation) { - setHasTransientState(false); - onFocusAnimationEnd(selected); - } - }); - mFocusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - setFocusAnimatedValue((Float) animation.getAnimatedValue()); - } - }); + @Override + public void onAnimationEnd(Animator animation) { + setHasTransientState(false); + onFocusAnimationEnd(selected); + } + }); + mFocusAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setFocusAnimatedValue((Float) animation.getAnimatedValue()); + } + }); mFocusAnimator.start(); } diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java index 2ecb6af7..76056ee4 100644 --- a/src/com/android/tv/menu/ChannelCardView.java +++ b/src/com/android/tv/menu/ChannelCardView.java @@ -26,19 +26,15 @@ import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.ImageLoader; - +import com.android.tv.util.images.ImageLoader; import java.util.Objects; -/** - * A view to render channel card. - */ +/** A view to render channel card. */ public class ChannelCardView extends BaseCardView<ChannelsRowItem> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; @@ -81,8 +77,13 @@ public class ChannelCardView extends BaseCardView<ChannelsRowItem> { @Override public void onBind(ChannelsRowItem item, boolean selected) { if (DEBUG) { - Log.d(TAG, "onBind(channelName=" + item.getChannel().getDisplayName() + ", selected=" - + selected + ")"); + Log.d( + TAG, + "onBind(channelName=" + + item.getChannel().getDisplayName() + + ", selected=" + + selected + + ")"); } updateChannel(item); updateProgram(); @@ -146,7 +147,8 @@ public class ChannelCardView extends BaseCardView<ChannelsRowItem> { return new ImageLoader.ImageLoaderCallback<ChannelCardView>(cardView) { @Override public void onBitmapLoaded(ChannelCardView cardView, @Nullable Bitmap posterArt) { - if (posterArt == null || cardView.mProgram == null + if (posterArt == null + || cardView.mProgram == null || program.getChannelId() != cardView.mProgram.getChannelId() || program.getChannelId() != cardView.mChannel.getId()) { return; @@ -160,7 +162,10 @@ public class ChannelCardView extends BaseCardView<ChannelsRowItem> { if (!TextUtils.equals(mPosterArtUri, posterArtUri)) { mPosterArtUri = posterArtUri; if (posterArtUri == null - || !mProgram.loadPosterArt(getContext(), mCardImageWidth, mCardImageHeight, + || !mProgram.loadPosterArt( + getContext(), + mCardImageWidth, + mCardImageHeight, createProgramPosterArtCallback(this, mProgram))) { mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setForeground(null); @@ -172,4 +177,4 @@ public class ChannelCardView extends BaseCardView<ChannelsRowItem> { mImageView.setImageBitmap(posterArt); mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient)); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java index bc5d6cfb..9cecb9c0 100644 --- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java +++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java @@ -23,24 +23,21 @@ import android.os.Message; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.util.Log; - import com.android.tv.R; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; +import com.android.tv.data.ChannelImpl; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; - +import com.android.tv.data.api.Channel; import java.util.List; -/** - * A poster image prefetcher to show the program poster art in the Channels row faster. - */ +/** A poster image prefetcher to show the program poster art in the Channels row faster. */ public class ChannelsPosterPrefetcher { private static final String TAG = "PosterPrefetcher"; private static final boolean DEBUG = false; private static final int MSG_PREFETCH_IMAGE = 1000; - private static final int ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS = 500; // 500 milliseconds + private static final int ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS = 500; // 500 milliseconds private final ProgramDataManager mProgramDataManager; private final ChannelsRowAdapter mChannelsAdapter; @@ -51,23 +48,19 @@ public class ChannelsPosterPrefetcher { private boolean isCanceled; - /** - * Create {@link ChannelsPosterPrefetcher} object with given parameters. - */ - public ChannelsPosterPrefetcher(Context context, ProgramDataManager programDataManager, - ChannelsRowAdapter adapter) { + /** Create {@link ChannelsPosterPrefetcher} object with given parameters. */ + public ChannelsPosterPrefetcher( + Context context, ProgramDataManager programDataManager, ChannelsRowAdapter adapter) { mProgramDataManager = programDataManager; mChannelsAdapter = adapter; - mPosterArtWidth = context.getResources().getDimensionPixelSize( - R.dimen.card_image_layout_width); - mPosterArtHeight = context.getResources().getDimensionPixelSize( - R.dimen.card_image_layout_height); + mPosterArtWidth = + context.getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); + mPosterArtHeight = + context.getResources().getDimensionPixelSize(R.dimen.card_image_layout_height); mContext = context.getApplicationContext(); } - /** - * Start prefetching of program poster art of recommendation. - */ + /** Start prefetching of program poster art of recommendation. */ public void prefetch() { SoftPreconditions.checkState(!isCanceled, TAG, "Prefetch called after cancel was called."); if (isCanceled) { @@ -79,13 +72,11 @@ public class ChannelsPosterPrefetcher { * prefetch the intermediate channels. So ignore previous schedule. */ mHandler.removeMessages(MSG_PREFETCH_IMAGE); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PREFETCH_IMAGE), - ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_PREFETCH_IMAGE), ONDEMAND_POSTER_PREFETCH_DELAY_MILLIS); } - /** - * Cancels pending and current prefetch requests. - */ + /** Cancels pending and current prefetch requests. */ public void cancel() { isCanceled = true; mHandler.removeCallbacksAndMessages(null); @@ -104,11 +95,14 @@ public class ChannelsPosterPrefetcher { return; } Channel channel = item.getChannel(); - if (!Channel.isValid(channel)) { + if (!ChannelImpl.isValid(channel)) { continue; } - channel.prefetchImage(mContext, Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, - mPosterArtWidth, mPosterArtHeight); + channel.prefetchImage( + mContext, + Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mPosterArtWidth, + mPosterArtHeight); Program program = mProgramDataManager.getCurrentProgram(channel.getId()); if (program != null) { program.prefetchPosterArt(mContext, mPosterArtWidth, mPosterArtHeight); @@ -116,8 +110,11 @@ public class ChannelsPosterPrefetcher { } } if (DEBUG) { - Log.d(TAG, "doPrefetchImages() finished. ImageLoader may still have async tasks for " - + "channels " + items); + Log.d( + TAG, + "doPrefetchImages() finished. ImageLoader may still have async tasks for " + + "channels " + + items); } } diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java index 490d73de..7d03bf2b 100644 --- a/src/com/android/tv/menu/ChannelsRow.java +++ b/src/com/android/tv/menu/ChannelsRow.java @@ -17,7 +17,6 @@ package com.android.tv.menu; import android.content.Context; - import com.android.tv.R; import com.android.tv.data.ProgramDataManager; import com.android.tv.recommendation.RecentChannelEvaluator; @@ -26,13 +25,9 @@ import com.android.tv.recommendation.Recommender; public class ChannelsRow extends ItemListRow { public static final String ID = ChannelsRow.class.getName(); - /** - * Minimum count for recent channels. - */ + /** Minimum count for recent channels. */ public static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5; - /** - * Maximum count for recent channels. - */ + /** Maximum count for recent channels. */ public static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10; private Recommender mTvRecommendation; @@ -41,25 +36,33 @@ public class ChannelsRow extends ItemListRow { public ChannelsRow(Context context, Menu menu, ProgramDataManager programDataManager) { super(context, menu, R.string.menu_title_channels, R.dimen.card_layout_height, null); - mTvRecommendation = new Recommender(getContext(), new Recommender.Listener() { - @Override - public void onRecommenderReady() { - mChannelsAdapter.update(); - mChannelsPosterPrefetcher.prefetch(); - } + mTvRecommendation = + new Recommender( + getContext(), + new Recommender.Listener() { + @Override + public void onRecommenderReady() { + mChannelsAdapter.update(); + mChannelsPosterPrefetcher.prefetch(); + } - @Override - public void onRecommendationChanged() { - mChannelsAdapter.update(); - mChannelsPosterPrefetcher.prefetch(); - } - }, true); + @Override + public void onRecommendationChanged() { + mChannelsAdapter.update(); + mChannelsPosterPrefetcher.prefetch(); + } + }, + true); mTvRecommendation.registerEvaluator(new RecentChannelEvaluator()); - mChannelsAdapter = new ChannelsRowAdapter(context, mTvRecommendation, - MIN_COUNT_FOR_RECENT_CHANNELS, MAX_COUNT_FOR_RECENT_CHANNELS); + mChannelsAdapter = + new ChannelsRowAdapter( + context, + mTvRecommendation, + MIN_COUNT_FOR_RECENT_CHANNELS, + MAX_COUNT_FOR_RECENT_CHANNELS); setAdapter(mChannelsAdapter); - mChannelsPosterPrefetcher = new ChannelsPosterPrefetcher(context, programDataManager, - mChannelsAdapter); + mChannelsPosterPrefetcher = + new ChannelsPosterPrefetcher(context, programDataManager, mChannelsAdapter); } @Override @@ -72,9 +75,7 @@ public class ChannelsRow extends ItemListRow { mChannelsPosterPrefetcher.cancel(); } - /** - * Handle the update event of the recent channel. - */ + /** Handle the update event of the recent channel. */ @Override public void onRecentChannelsChanged() { mChannelsPosterPrefetcher.prefetch(); diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 7ff44ea6..8536ef1f 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -20,25 +20,20 @@ import android.content.Context; import android.content.Intent; import android.media.tv.TvInputInfo; import android.view.View; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; +import com.android.tv.data.ChannelImpl; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.recommendation.Recommender; -import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; - import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; -/** - * An adapter of the Channels row. - */ +/** An adapter of the Channels row. */ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> { // There are four special cards: guide, setup, dvr, applink. private static final int SIZE_OF_VIEW_TYPE = 5; @@ -50,60 +45,66 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels private final int mMaxCount; private final int mMinCount; - private final View.OnClickListener mGuideOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_program_guide); - getMainActivity().getOverlayManager().showProgramGuide(); - } - }; + private final View.OnClickListener mGuideOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + mTracker.sendMenuClicked(R.string.channels_item_program_guide); + getMainActivity().getOverlayManager().showProgramGuide(); + } + }; - private final View.OnClickListener mSetupOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_setup); - getMainActivity().getOverlayManager().showSetupFragment(); - } - }; + private final View.OnClickListener mSetupOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + mTracker.sendMenuClicked(R.string.channels_item_setup); + getMainActivity().getOverlayManager().showSetupFragment(); + } + }; - private final View.OnClickListener mDvrOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_dvr); - getMainActivity().getOverlayManager().showDvrManager(); - } - }; + private final View.OnClickListener mDvrOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + mTracker.sendMenuClicked(R.string.channels_item_dvr); + getMainActivity().getOverlayManager().showDvrManager(); + } + }; - private final View.OnClickListener mAppLinkOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mTracker.sendMenuClicked(R.string.channels_item_app_link); - Intent intent = ((AppLinkCardView) view).getIntent(); - if (intent != null) { - getMainActivity().startActivitySafe(intent); - } - } - }; + private final View.OnClickListener mAppLinkOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + mTracker.sendMenuClicked(R.string.channels_item_app_link); + Intent intent = ((AppLinkCardView) view).getIntent(); + if (intent != null) { + getMainActivity().startActivitySafe(intent); + } + } + }; - private final View.OnClickListener mChannelOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - // Always send the label "Channels" because the channel ID or name or number might be - // sensitive. - mTracker.sendMenuClicked(R.string.menu_title_channels); - getMainActivity().tuneToChannel((Channel) view.getTag()); - getMainActivity().hideOverlaysForTune(); - } - }; + private final View.OnClickListener mChannelOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + // Always send the label "Channels" because the channel ID or name or number + // might be + // sensitive. + mTracker.sendMenuClicked(R.string.menu_title_channels); + getMainActivity().tuneToChannel((Channel) view.getTag()); + getMainActivity().hideOverlaysForTune(); + } + }; - public ChannelsRowAdapter(Context context, Recommender recommender, - int minCount, int maxCount) { + public ChannelsRowAdapter( + Context context, Recommender recommender, int minCount, int maxCount) { super(context); mContext = context; - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mTracker = appSingletons.getTracker(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mTracker = tvSingletons.getTracker(); if (CommonFeatures.DVR.isEnabled(context)) { - mDvrDataManager = appSingletons.getDvrDataManager(); + mDvrDataManager = tvSingletons.getDvrDataManager(); } else { mDvrDataManager = null; } @@ -168,7 +169,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels } if (needToShowAppLinkItem()) { ChannelsRowItem.APP_LINK_ITEM.setChannel( - new Channel.Builder(getMainActivity().getCurrentChannel()).build()); + new ChannelImpl.Builder(getMainActivity().getCurrentChannel()).build()); items.add(ChannelsRowItem.APP_LINK_ITEM); } for (Channel channel : getRecentChannels()) { @@ -189,10 +190,11 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels ++currentIndex; } if (updateItem(needToShowAppLinkItem(), ChannelsRowItem.APP_LINK_ITEM, currentIndex)) { - if (!getMainActivity().getCurrentChannel() + if (!getMainActivity() + .getCurrentChannel() .hasSameReadOnlyInfo(ChannelsRowItem.APP_LINK_ITEM.getChannel())) { ChannelsRowItem.APP_LINK_ITEM.setChannel( - new Channel.Builder(getMainActivity().getCurrentChannel()).build()); + new ChannelImpl.Builder(getMainActivity().getCurrentChannel()).build()); notifyItemChanged(currentIndex); } ++currentIndex; @@ -213,9 +215,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels } } - /** - * Returns {@code true} if the item should be shown. - */ + /** Returns {@code true} if the item should be shown. */ private boolean updateItem(boolean needToShow, ChannelsRowItem item, int index) { List<ChannelsRowItem> items = getItemList(); boolean isItemInList = index < items.size() && item.equals(items.get(index)); @@ -230,14 +230,14 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels } private boolean needToShowSetupItem() { - TvInputManagerHelper inputManager = - TvApplication.getSingletons(mContext).getTvInputManagerHelper(); - return SetupUtils.getInstance(mContext).hasNewInput(inputManager); + TvSingletons singletons = TvSingletons.getSingletons(mContext); + TvInputManagerHelper inputManager = singletons.getTvInputManagerHelper(); + return singletons.getSetupUtils().hasNewInput(inputManager); } private boolean needToShowDvrItem() { TvInputManagerHelper inputManager = - TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + TvSingletons.getSingletons(mContext).getTvInputManagerHelper(); if (mDvrDataManager != null) { for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) { if (info.canRecord()) { @@ -250,7 +250,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels private boolean needToShowAppLinkItem() { TvInputManagerHelper inputManager = - TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + TvSingletons.getSingletons(mContext).getTvInputManagerHelper(); Channel currentChannel = getMainActivity().getCurrentChannel(); return currentChannel != null && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE @@ -289,8 +289,10 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channels private static boolean addChannelToList( List<Channel> channelList, Channel channel, long currentChannelId) { - if (channel == null || channel.getId() == currentChannelId - || channelList.contains(channel) || !channel.isBrowsable()) { + if (channel == null + || channel.getId() == currentChannelId + || channelList.contains(channel) + || !channel.isBrowsable()) { return false; } channelList.add(channel); diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java index c35189ec..608bb36e 100644 --- a/src/com/android/tv/menu/ChannelsRowItem.java +++ b/src/com/android/tv/menu/ChannelsRowItem.java @@ -17,14 +17,10 @@ package com.android.tv.menu; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - import com.android.tv.R; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; -/** - * A class for the items in channels row. - */ +/** A class for the items in channels row. */ public class ChannelsRowItem { /** The item ID for guide item */ public static final int GUIDE_ITEM_ID = -1; @@ -62,31 +58,23 @@ public class ChannelsRowItem { mLayoutId = layoutId; } - /** - * Returns the channel for this item. - */ + /** Returns the channel for this item. */ @NonNull public Channel getChannel() { return mChannel; } - /** - * Sets the channel. - */ + /** Sets the channel. */ public void setChannel(@NonNull Channel channel) { mChannel = channel; } - /** - * Returns the layout resource ID to represent this item. - */ + /** Returns the layout resource ID to represent this item. */ public int getLayoutId() { return mLayoutId; } - /** - * Returns the unique ID for this item. - */ + /** Returns the unique ID for this item. */ public long getItemId() { return mItemId; } @@ -94,8 +82,12 @@ public class ChannelsRowItem { @Override public String toString() { return "ChannelsRowItem{" - + "itemId=" + mItemId - + ", layoutId=" + mLayoutId - + ", channel=" + mChannel + "}"; + + "itemId=" + + mItemId + + ", layoutId=" + + mLayoutId + + ", channel=" + + mChannel + + "}"; } } diff --git a/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java b/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java index f69d5e86..7da26916 100644 --- a/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java +++ b/src/com/android/tv/menu/CustomizableOptionsRowAdapter.java @@ -17,15 +17,11 @@ package com.android.tv.menu; import android.content.Context; - -import com.android.tv.customization.CustomAction; - +import com.android.tv.common.customization.CustomAction; import java.util.ArrayList; import java.util.List; -/** - * An adapter of options that can accepts customization data. - */ +/** An adapter of options that can accepts customization data. */ public abstract class CustomizableOptionsRowAdapter extends OptionsRowAdapter { private final List<CustomAction> mCustomActions; @@ -54,8 +50,9 @@ public abstract class CustomizableOptionsRowAdapter extends OptionsRowAdapter { // Type of MenuAction should be unique in the Adapter. int type = -(i + 1); CustomAction customAction = mCustomActions.get(i); - MenuAction action = new MenuAction( - customAction.getTitle(), type, customAction.getIconDrawable()); + MenuAction action = + new MenuAction( + customAction.getTitle(), type, customAction.getIconDrawable()); if (customAction.isFront()) { actions.add(position++, action); diff --git a/src/com/android/tv/menu/IMenuView.java b/src/com/android/tv/menu/IMenuView.java index 87c5d9f6..19ebc739 100644 --- a/src/com/android/tv/menu/IMenuView.java +++ b/src/com/android/tv/menu/IMenuView.java @@ -17,33 +17,26 @@ package com.android.tv.menu; import com.android.tv.menu.Menu.MenuShowReason; - import java.util.List; -/** - * An base interface for menu view. - */ +/** An base interface for menu view. */ public interface IMenuView { - /** - * Sets menu rows. - */ + /** Sets menu rows. */ void setMenuRows(List<MenuRow> menuRows); /** * Shows the main menu. * - * <p> The inherited class should show the menu and select the row corresponding to - * {@code rowIdToSelect}. If the menu is already visible, change the current selection to the - * given row. + * <p>The inherited class should show the menu and select the row corresponding to {@code + * rowIdToSelect}. If the menu is already visible, change the current selection to the given + * row. * * @param reason A reason why this is called. See {@link MenuShowReason}. * @param rowIdToSelect An ID of the row which corresponds to the {@code reason}. */ void onShow(@MenuShowReason int reason, String rowIdToSelect, Runnable runnableAfterShow); - /** - * Hides the main menu - */ + /** Hides the main menu */ void onHide(); /** @@ -60,8 +53,6 @@ public interface IMenuView { */ boolean update(String rowId, boolean menuActive); - /** - * Checks if the menu view is visible or not. - */ + /** Checks if the menu view is visible or not. */ boolean isVisible(); } diff --git a/src/com/android/tv/menu/ItemListRow.java b/src/com/android/tv/menu/ItemListRow.java index faa611fa..2993d085 100644 --- a/src/com/android/tv/menu/ItemListRow.java +++ b/src/com/android/tv/menu/ItemListRow.java @@ -17,33 +17,37 @@ package com.android.tv.menu; import android.content.Context; - import com.android.tv.R; import com.android.tv.menu.ItemListRowView.ItemListAdapter; /** - * A menu item which is used to represents the list of the items. - * A list will be displayed by a HorizontalGridView with cards, so an adapter - * for the GridView is necessary. + * A menu item which is used to represents the list of the items. A list will be displayed by a + * HorizontalGridView with cards, so an adapter for the GridView is necessary. */ @SuppressWarnings("rawtypes") public class ItemListRow extends MenuRow { private ItemListAdapter mAdapter; - public ItemListRow(Context context, Menu menu, int titleResId, int itemHeightResId, + public ItemListRow( + Context context, + Menu menu, + int titleResId, + int itemHeightResId, ItemListAdapter adapter) { this(context, menu, context.getString(titleResId), itemHeightResId, adapter); } - public ItemListRow(Context context, Menu menu, String title, int itemHeightResId, + public ItemListRow( + Context context, + Menu menu, + String title, + int itemHeightResId, ItemListAdapter adapter) { super(context, menu, title, itemHeightResId); mAdapter = adapter; } - /** - * Returns the adapter. - */ + /** Returns the adapter. */ public ItemListAdapter<?> getAdapter() { return mAdapter; } diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java index cbeee936..7042324d 100644 --- a/src/com/android/tv/menu/ItemListRowView.java +++ b/src/com/android/tv/menu/ItemListRowView.java @@ -25,25 +25,24 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.util.ViewCache; - import java.util.Collections; import java.util.List; -/** - * A view that shows a title and list view. - */ +/** A view that shows a title and list view. */ public class ItemListRowView extends MenuRowView implements OnChildSelectedListener { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; public interface CardView<T> { void onBind(T row, boolean selected); + void onRecycled(); + void onSelected(); + void onDeselected(); } @@ -115,7 +114,7 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe } } - public static abstract class ItemListAdapter<T> + public abstract static class ItemListAdapter<T> extends RecyclerView.Adapter<ItemListAdapter.MyViewHolder> { private final MainActivity mMainActivity; private final LayoutInflater mLayoutInflater; @@ -129,25 +128,20 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe } /** - * In most cases, implementation should call {@link #setItemList(java.util.List)} with - * newly update item list. + * In most cases, implementation should call {@link #setItemList(java.util.List)} with newly + * update item list. */ public abstract void update(); - /** - * Gets layout resource ID. It'll be used in {@link #onCreateViewHolder}. - */ + /** Gets layout resource ID. It'll be used in {@link #onCreateViewHolder}. */ protected abstract int getLayoutResId(int viewType); - /** - * Releases all the resources which need to be released. - */ - public void release() { - } + /** Releases all the resources which need to be released. */ + public void release() {} /** - * The initial position of list that will be selected when the main menu appears. - * By default, the first item is initially selected. + * The initial position of list that will be selected when the main menu appears. By + * default, the first item is initially selected. */ public int getInitialPosition() { return 0; @@ -169,8 +163,8 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe * <p>This sends an item change event, not a structural change event. The items of the same * positions retain the same identity. * - * <p>If there's any structural change and relayout and rebind is needed, call - * {@link #notifyDataSetChanged} explicitly. + * <p>If there's any structural change and relayout and rebind is needed, call {@link + * #notifyDataSetChanged} explicitly. */ protected void setItemList(List<T> itemList) { int oldSize = mItemList.size(); @@ -197,24 +191,21 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe return mItemList.size(); } - /** - * Returns the position of the item. - */ + /** Returns the position of the item. */ protected int getItemPosition(T item) { return mItemList.indexOf(item); } - /** - * Returns {@code true} if the item list contains the item, otherwise {@code false}. - */ + /** Returns {@code true} if the item list contains the item, otherwise {@code false}. */ protected boolean containsItem(T item) { return mItemList.contains(item); } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = ViewCache.getInstance().getOrCreateView( - mLayoutInflater, getLayoutResId(viewType), parent); + View view = + ViewCache.getInstance() + .getOrCreateView(mLayoutInflater, getLayoutResId(viewType), parent); return new MyViewHolder(view); } diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index e373de61..19a93dbc 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -21,27 +21,23 @@ import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.Resources; -import android.os.Looper; -import android.os.Message; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.HorizontalGridView; import android.util.Log; - +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import com.android.tv.ChannelTuner; import com.android.tv.R; -import com.android.tv.TvApplication; import com.android.tv.TvOptionsManager; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; -import com.android.tv.common.TvCommonUtils; -import com.android.tv.common.WeakHandler; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.DurationTimer; import com.android.tv.menu.MenuRowFactory.PartnerRow; import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; -import com.android.tv.util.DurationTimer; +import com.android.tv.ui.hideable.AutoHideScheduler; import com.android.tv.util.ViewCache; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -49,19 +45,25 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * A class which controls the menu. - */ -public class Menu { +/** A class which controls the menu. */ +public class Menu implements AccessibilityStateChangeListener { private static final String TAG = "Menu"; private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE, - REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND, - REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, - REASON_PLAY_CONTROLS_JUMP_TO_NEXT}) + @IntDef({ + REASON_NONE, + REASON_GUIDE, + REASON_PLAY_CONTROLS_PLAY, + REASON_PLAY_CONTROLS_PAUSE, + REASON_PLAY_CONTROLS_PLAY_PAUSE, + REASON_PLAY_CONTROLS_REWIND, + REASON_PLAY_CONTROLS_FAST_FORWARD, + REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, + REASON_PLAY_CONTROLS_JUMP_TO_NEXT + }) public @interface MenuShowReason {} + public static final int REASON_NONE = 0; public static final int REASON_GUIDE = 1; public static final int REASON_PLAY_CONTROLS_PLAY = 2; @@ -73,6 +75,7 @@ public class Menu { public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; private static final List<String> sRowIdListForReason = new ArrayList<>(); + static { sRowIdListForReason.add(null); // REASON_NONE sRowIdListForReason.add(ChannelsRow.ID); // REASON_GUIDE @@ -86,6 +89,7 @@ public class Menu { } private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>(); + static { PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1); PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1); @@ -97,15 +101,13 @@ public class Menu { private static final String SCREEN_NAME = "Menu"; - private static final int MSG_HIDE_MENU = 1000; - private final Context mContext; private final IMenuView mMenuView; private final Tracker mTracker; private final DurationTimer mVisibleTimer = new DurationTimer(); private final long mShowDurationMillis; private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener; - private final WeakHandler<Menu> mHandler = new MenuWeakHandler(this, Looper.getMainLooper()); + private final AutoHideScheduler mAutoHideScheduler; private final MenuUpdater mMenuUpdater; private final List<MenuRow> mMenuRows = new ArrayList<>(); @@ -116,17 +118,24 @@ public class Menu { private boolean mAnimationDisabledForTest; @VisibleForTesting - Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, + Menu( + Context context, + IMenuView menuView, + MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); } - public Menu(Context context, TunableTvView tvView, TvOptionsManager optionsManager, - IMenuView menuView, MenuRowFactory menuRowFactory, + public Menu( + Context context, + TunableTvView tvView, + TvOptionsManager optionsManager, + IMenuView menuView, + MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { mContext = context; mMenuView = menuView; - mTracker = TvApplication.getSingletons(context).getTracker(); + mTracker = TvSingletons.getSingletons(context).getTracker(); mMenuUpdater = new MenuUpdater(this, tvView, optionsManager); Resources res = context.getResources(); mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); @@ -134,12 +143,13 @@ public class Menu { mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter); mShowAnimator.setTarget(mMenuView); mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit); - mHideAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - hideInternal(); - } - }); + mHideAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + hideInternal(); + } + }); mHideAnimator.setTarget(mMenuView); // Build menu rows addMenuRow(menuRowFactory.createMenuRow(this, PlayControlsRow.class)); @@ -147,6 +157,7 @@ public class Menu { addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class)); addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class)); mMenuView.setMenuRows(mMenuRows); + mAutoHideScheduler = new AutoHideScheduler(context, () -> hide(true)); } /** @@ -163,20 +174,16 @@ public class Menu { } } - /** - * Call this method to end the lifetime of the menu. - */ + /** Call this method to end the lifetime of the menu. */ public void release() { mMenuUpdater.release(); for (MenuRow row : mMenuRows) { row.release(); } - mHandler.removeCallbacksAndMessages(null); + mAutoHideScheduler.cancel(); } - /** - * Preloads the item view used for the menu. - */ + /** Preloads the item view used for the menu. */ public void preloadItemViews() { HorizontalGridView fakeParent = new HorizontalGridView(mContext); for (int id : PRELOAD_VIEW_IDS.keySet()) { @@ -201,20 +208,23 @@ public class Menu { mOnMenuVisibilityChangeListener.onMenuVisibilityChange(true); } String rowIdToSelect = sRowIdListForReason.get(reason); - mMenuView.onShow(reason, rowIdToSelect, mAnimationDisabledForTest ? null : new Runnable() { - @Override - public void run() { - if (isActive()) { - mShowAnimator.start(); - } - } - }); + mMenuView.onShow( + reason, + rowIdToSelect, + mAnimationDisabledForTest + ? null + : new Runnable() { + @Override + public void run() { + if (isActive()) { + mShowAnimator.start(); + } + } + }); scheduleHide(); } - /** - * Closes the menu. - */ + /** Closes the menu. */ public void hide(boolean withAnimation) { if (mShowAnimator.isStarted()) { mShowAnimator.cancel(); @@ -225,7 +235,7 @@ public class Menu { if (mAnimationDisabledForTest) { withAnimation = false; } - mHandler.removeMessages(MSG_HIDE_MENU); + mAutoHideScheduler.cancel(); if (withAnimation) { if (!mHideAnimator.isStarted()) { mHideAnimator.start(); @@ -246,26 +256,21 @@ public class Menu { } } - /** - * Schedules to hide the menu in some seconds. - */ + /** Schedules to hide the menu in some seconds. */ public void scheduleHide() { - mHandler.removeMessages(MSG_HIDE_MENU); - if (!mKeepVisible) { - mHandler.sendEmptyMessageDelayed(MSG_HIDE_MENU, mShowDurationMillis); - } + mAutoHideScheduler.schedule(mShowDurationMillis); } /** - * Called when the caller wants the main menu to be kept visible or not. - * If {@code keepVisible} is set to {@code true}, the hide schedule doesn't close the main menu, - * but calling {@link #hide} still hides it. - * If {@code keepVisible} is set to {@code false}, the hide schedule works as usual. + * Called when the caller wants the main menu to be kept visible or not. If {@code keepVisible} + * is set to {@code true}, the hide schedule doesn't close the main menu, but calling {@link + * #hide} still hides it. If {@code keepVisible} is set to {@code false}, the hide schedule + * works as usual. */ public void setKeepVisible(boolean keepVisible) { mKeepVisible = keepVisible; if (mKeepVisible) { - mHandler.removeMessages(MSG_HIDE_MENU); + mAutoHideScheduler.cancel(); } else if (isActive()) { scheduleHide(); } @@ -273,12 +278,10 @@ public class Menu { @VisibleForTesting boolean isHideScheduled() { - return mHandler.hasMessages(MSG_HIDE_MENU); + return mAutoHideScheduler.isScheduled(); } - /** - * Returns {@code true} if the menu is open and not hiding. - */ + /** Returns {@code true} if the menu is open and not hiding. */ public boolean isActive() { return mMenuView.isVisible() && !mHideAnimator.isStarted(); } @@ -303,9 +306,7 @@ public class Menu { return mMenuView.update(rowId, isActive()); } - /** - * This method is called when channels are changed. - */ + /** This method is called when channels are changed. */ public void onRecentChannelsChanged() { if (DEBUG) Log.d(TAG, "onRecentChannelsChanged"); for (MenuRow row : mMenuRows) { @@ -313,42 +314,28 @@ public class Menu { } } - /** - * This method is called when the stream information is changed. - */ + /** This method is called when the stream information is changed. */ public void onStreamInfoChanged() { if (DEBUG) Log.d(TAG, "update options row in main menu"); mMenuUpdater.onStreamInfoChanged(); } + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mAutoHideScheduler.onAccessibilityStateChanged(enabled); + } + @VisibleForTesting void disableAnimationForTest() { - if (!TvCommonUtils.isRunningInTest()) { + if (!CommonUtils.isRunningInTest()) { throw new RuntimeException("Animation may only be enabled/disabled during tests."); } mAnimationDisabledForTest = true; } - /** - * A listener which receives the notification when the menu is visible/invisible. - */ - public static abstract class OnMenuVisibilityChangeListener { - /** - * Called when the menu becomes visible/invisible. - */ + /** A listener which receives the notification when the menu is visible/invisible. */ + public abstract static class OnMenuVisibilityChangeListener { + /** Called when the menu becomes visible/invisible. */ public abstract void onMenuVisibilityChange(boolean visible); } - - private static class MenuWeakHandler extends WeakHandler<Menu> { - public MenuWeakHandler(Menu menu, Looper mainLooper) { - super(mainLooper, menu); - } - - @Override - public void handleMessage(Message msg, @NonNull Menu menu) { - if (msg.what == MSG_HIDE_MENU) { - menu.hide(true); - } - } - } } diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java index b4356059..52372535 100644 --- a/src/com/android/tv/menu/MenuAction.java +++ b/src/com/android/tv/menu/MenuAction.java @@ -19,37 +19,47 @@ package com.android.tv.menu; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; - import com.android.tv.R; import com.android.tv.TvOptionsManager; import com.android.tv.TvOptionsManager.OptionType; -/** - * A class to define possible actions from main menu. - */ +/** A class to define possible actions from main menu. */ public class MenuAction { // Actions in the TV option row. public static final MenuAction SELECT_CLOSED_CAPTION_ACTION = - new MenuAction(R.string.options_item_closed_caption, + new MenuAction( + R.string.options_item_closed_caption, TvOptionsManager.OPTION_CLOSED_CAPTIONS, R.drawable.ic_tvoption_cc); public static final MenuAction SELECT_DISPLAY_MODE_ACTION = - new MenuAction(R.string.options_item_display_mode, TvOptionsManager.OPTION_DISPLAY_MODE, + new MenuAction( + R.string.options_item_display_mode, + TvOptionsManager.OPTION_DISPLAY_MODE, R.drawable.ic_tvoption_aspect); public static final MenuAction SYSTEMWIDE_PIP_ACTION = - new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_SYSTEMWIDE_PIP, + new MenuAction( + R.string.options_item_pip, + TvOptionsManager.OPTION_SYSTEMWIDE_PIP, R.drawable.ic_tvoption_pip); public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION = - new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO, + new MenuAction( + R.string.options_item_multi_audio, + TvOptionsManager.OPTION_MULTI_AUDIO, R.drawable.ic_tvoption_multi_track); public static final MenuAction MORE_CHANNELS_ACTION = - new MenuAction(R.string.options_item_more_channels, - TvOptionsManager.OPTION_MORE_CHANNELS, R.drawable.ic_store); + new MenuAction( + R.string.options_item_more_channels, + TvOptionsManager.OPTION_MORE_CHANNELS, + R.drawable.ic_store); public static final MenuAction DEV_ACTION = - new MenuAction(R.string.options_item_developer, - TvOptionsManager.OPTION_DEVELOPER, R.drawable.ic_developer_mode_tv_white_48dp); + new MenuAction( + R.string.options_item_developer, + TvOptionsManager.OPTION_DEVELOPER, + R.drawable.ic_developer_mode_tv_white_48dp); public static final MenuAction SETTINGS_ACTION = - new MenuAction(R.string.options_item_settings, TvOptionsManager.OPTION_SETTINGS, + new MenuAction( + R.string.options_item_settings, + TvOptionsManager.OPTION_SETTINGS, R.drawable.ic_settings); private final String mActionName; @@ -60,18 +70,14 @@ public class MenuAction { private int mDrawableResId; private boolean mEnabled = true; - /** - * Sets the action description. Returns {@code trye} if the description is changed. - */ + /** Sets the action description. Returns {@code trye} if the description is changed. */ public static boolean setActionDescription(MenuAction action, String actionDescription) { String oldDescription = action.mActionDescription; action.mActionDescription = actionDescription; return !TextUtils.equals(action.mActionDescription, oldDescription); } - /** - * Enables or disables the action. Returns {@code true} if the value is changed. - */ + /** Enables or disables the action. Returns {@code true} if the value is changed. */ public static boolean setEnabled(MenuAction action, boolean enabled) { boolean changed = action.mEnabled != enabled; action.mEnabled = enabled; @@ -105,13 +111,12 @@ public class MenuAction { return mActionDescription; } - @OptionType public int getType() { + @OptionType + public int getType() { return mType; } - /** - * Returns Drawable. - */ + /** Returns Drawable. */ public Drawable getDrawable(Context context) { if (mDrawable == null) { mDrawable = context.getDrawable(mDrawableResId); diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java index 173d4004..a600f704 100644 --- a/src/com/android/tv/menu/MenuLayoutManager.java +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -34,11 +34,9 @@ import android.util.Property; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.common.SoftPreconditions; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -47,9 +45,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; -/** - * A view that represents TV main menu. - */ +/** A view that represents TV main menu. */ @UiThread public class MenuLayoutManager { static final String TAG = "MenuLayoutManager"; @@ -93,24 +89,22 @@ public class MenuLayoutManager { Resources res = context.getResources(); mRowAlignFromBottom = res.getDimensionPixelOffset(R.dimen.menu_row_align_from_bottom); mRowContentsPaddingTop = res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_top); - mRowContentsPaddingBottomMax = res.getDimensionPixelOffset( - R.dimen.menu_row_contents_padding_bottom_max); - mRowTitleTextDescenderHeight = res.getDimensionPixelOffset( - R.dimen.menu_row_title_text_descender_height); + mRowContentsPaddingBottomMax = + res.getDimensionPixelOffset(R.dimen.menu_row_contents_padding_bottom_max); + mRowTitleTextDescenderHeight = + res.getDimensionPixelOffset(R.dimen.menu_row_title_text_descender_height); mMenuMarginBottomMin = res.getDimensionPixelOffset(R.dimen.menu_margin_bottom_min); mRowTitleHeight = res.getDimensionPixelSize(R.dimen.menu_row_title_height); mRowScrollUpAnimationOffset = res.getDimensionPixelOffset(R.dimen.menu_row_scroll_up_anim_offset); mRowAnimationDuration = res.getInteger(R.integer.menu_row_selection_anim_duration); - mOldContentsFadeOutDuration = res.getInteger( - R.integer.menu_previous_contents_fade_out_duration); - mCurrentContentsFadeInDuration = res.getInteger( - R.integer.menu_current_contents_fade_in_duration); + mOldContentsFadeOutDuration = + res.getInteger(R.integer.menu_previous_contents_fade_out_duration); + mCurrentContentsFadeInDuration = + res.getInteger(R.integer.menu_current_contents_fade_in_duration); } - /** - * Sets the menu rows and views. - */ + /** Sets the menu rows and views. */ public void setMenuRowsAndViews(List<MenuRow> menuRows, List<MenuRowView> menuRowViews) { mMenuRows.clear(); mMenuRows.addAll(menuRows); @@ -181,22 +175,54 @@ public class MenuLayoutManager { for (MenuRowView view : mMenuRowViews) { View title = view.getChildAt(0); View contents = view.getChildAt(1); - Log.d(TAG, prefix + " position=" + position++ - + " rowView={visiblility=" + view.getVisibility() - + ", alpha=" + view.getAlpha() - + ", translationY=" + view.getTranslationY() - + ", left=" + view.getLeft() + ", top=" + view.getTop() - + ", right=" + view.getRight() + ", bottom=" + view.getBottom() - + "}, title={visiblility=" + title.getVisibility() - + ", alpha=" + title.getAlpha() - + ", translationY=" + title.getTranslationY() - + ", left=" + title.getLeft() + ", top=" + title.getTop() - + ", right=" + title.getRight() + ", bottom=" + title.getBottom() - + "}, contents={visiblility=" + contents.getVisibility() - + ", alpha=" + contents.getAlpha() - + ", translationY=" + contents.getTranslationY() - + ", left=" + contents.getLeft() + ", top=" + contents.getTop() - + ", right=" + contents.getRight() + ", bottom=" + contents.getBottom()+ "}"); + Log.d( + TAG, + prefix + + " position=" + + position++ + + " rowView={visiblility=" + + view.getVisibility() + + ", alpha=" + + view.getAlpha() + + ", translationY=" + + view.getTranslationY() + + ", left=" + + view.getLeft() + + ", top=" + + view.getTop() + + ", right=" + + view.getRight() + + ", bottom=" + + view.getBottom() + + "}, title={visiblility=" + + title.getVisibility() + + ", alpha=" + + title.getAlpha() + + ", translationY=" + + title.getTranslationY() + + ", left=" + + title.getLeft() + + ", top=" + + title.getTop() + + ", right=" + + title.getRight() + + ", bottom=" + + title.getBottom() + + "}, contents={visiblility=" + + contents.getVisibility() + + ", alpha=" + + contents.getAlpha() + + ", translationY=" + + contents.getTranslationY() + + ", left=" + + contents.getLeft() + + ", top=" + + contents.getTop() + + ", right=" + + contents.getRight() + + ", bottom=" + + contents.getBottom() + + "}"); } } @@ -204,14 +230,14 @@ public class MenuLayoutManager { * Checks if the view will take up space for the layout not. * * @param position The index of the menu row view in the list. This is not the index of the view - * in the screen. + * in the screen. * @param view The menu row view. * @param rowsToAdd The menu row views to be added in the next layout process. * @param rowsToRemove The menu row views to be removed in the next layout process. * @return {@code true} if the view will take up space for the layout, otherwise {@code false}. */ - private boolean isVisibleInLayout(int position, MenuRowView view, List<Integer> rowsToAdd, - List<Integer> rowsToRemove) { + private boolean isVisibleInLayout( + int position, MenuRowView view, List<Integer> rowsToAdd, List<Integer> rowsToRemove) { // Checks if the view will be visible or not. return (view.getVisibility() != View.GONE && !rowsToRemove.contains(position)) || rowsToAdd.contains(position); @@ -226,8 +252,8 @@ public class MenuLayoutManager { * @param bottom The bottom coordinate of the menu view. */ private List<Rect> getViewLayouts(int left, int top, int right, int bottom) { - return getViewLayouts(left, top, right, bottom, Collections.emptyList(), - Collections.emptyList()); + return getViewLayouts( + left, top, right, bottom, Collections.emptyList(), Collections.emptyList()); } /** @@ -247,8 +273,13 @@ public class MenuLayoutManager { * @param rowsToRemove The menu row views to be removed in the next layout process. * @return the layout bounds of the menu row views. */ - private List<Rect> getViewLayouts(int left, int top, int right, int bottom, - List<Integer> rowsToAdd, List<Integer> rowsToRemove) { + private List<Rect> getViewLayouts( + int left, + int top, + int right, + int bottom, + List<Integer> rowsToAdd, + List<Integer> rowsToRemove) { // The coordinates should be relative to the parent. int relativeLeft = 0; int relateiveRight = right - left; @@ -262,18 +293,29 @@ public class MenuLayoutManager { // Calculate for the selected row first. // The distance between the bottom of the screen and the vertical center of the contents // should be kept fixed. For more information, please see the redlines. - int childTop = relativeBottom - mRowAlignFromBottom - rowContentsHeight / 2 - - mRowContentsPaddingTop - rowTitleHeight; + int childTop = + relativeBottom + - mRowAlignFromBottom + - rowContentsHeight / 2 + - mRowContentsPaddingTop + - rowTitleHeight; int childBottom = relativeBottom; int position = mSelectedPosition + 1; for (; position < count; ++position) { // Find and layout the next row to calculate the bottom line of the selected row. MenuRowView nextView = mMenuRowViews.get(position); if (isVisibleInLayout(position, nextView, rowsToAdd, rowsToRemove)) { - int nextTitleTopMax = relativeBottom - mMenuMarginBottomMin - rowTitleHeight - + mRowTitleTextDescenderHeight; - int childBottomMax = relativeBottom - mRowAlignFromBottom + rowContentsHeight / 2 - + mRowContentsPaddingBottomMax - rowTitleHeight; + int nextTitleTopMax = + relativeBottom + - mMenuMarginBottomMin + - rowTitleHeight + + mRowTitleTextDescenderHeight; + int childBottomMax = + relativeBottom + - mRowAlignFromBottom + + rowContentsHeight / 2 + + mRowContentsPaddingBottomMax + - rowTitleHeight; childBottom = Math.min(nextTitleTopMax, childBottomMax); layouts.add(new Rect(relativeLeft, childBottom, relateiveRight, relativeBottom)); break; @@ -309,19 +351,22 @@ public class MenuLayoutManager { return layouts; } - /** - * Move the current selection to the given {@code position}. - */ + /** Move the current selection to the given {@code position}. */ public void setSelectedPosition(int position) { if (DEBUG) { - Log.d(TAG, "setSelectedPosition(position=" + position + ") {previousPosition=" - + mSelectedPosition + "}"); + Log.d( + TAG, + "setSelectedPosition(position=" + + position + + ") {previousPosition=" + + mSelectedPosition + + "}"); } if (mSelectedPosition == position) { return; } boolean indexValid = Utils.isIndexValid(mMenuRowViews, position); - SoftPreconditions.checkArgument(indexValid, TAG, "position " + position); + SoftPreconditions.checkArgument(indexValid, TAG, "position %s ", position); if (!indexValid) { return; } @@ -347,13 +392,18 @@ public class MenuLayoutManager { } /** - * Move the current selection to the given {@code position} with animation. - * The animation specification is included in http://b/21069476 + * Move the current selection to the given {@code position} with animation. The animation + * specification is included in http://b/21069476 */ public void setSelectedPositionSmooth(final int position) { if (DEBUG) { - Log.d(TAG, "setSelectedPositionSmooth(position=" + position + ") {previousPosition=" - + mSelectedPosition + "}"); + Log.d( + TAG, + "setSelectedPositionSmooth(position=" + + position + + ") {previousPosition=" + + mSelectedPosition + + "}"); } if (mMenuView.getVisibility() != View.VISIBLE) { setSelectedPosition(position); @@ -363,13 +413,13 @@ public class MenuLayoutManager { return; } boolean oldIndexValid = Utils.isIndexValid(mMenuRowViews, mSelectedPosition); - SoftPreconditions - .checkState(oldIndexValid, TAG, "No previous selection: " + mSelectedPosition); + SoftPreconditions.checkState( + oldIndexValid, TAG, "No previous selection: " + mSelectedPosition); if (!oldIndexValid) { return; } boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position); - SoftPreconditions.checkArgument(newIndexValid, TAG, "position " + position); + SoftPreconditions.checkArgument(newIndexValid, TAG, "position %s", position); if (!newIndexValid) { return; } @@ -415,8 +465,7 @@ public class MenuLayoutManager { mMenuView.requestFocus(); if (mTempTitleViewForOld == null) { // Initialize here because we don't know when the views are inflated. - mTempTitleViewForOld = - (TextView) mMenuView.findViewById(R.id.temp_title_for_old); + mTempTitleViewForOld = (TextView) mMenuView.findViewById(R.id.temp_title_for_old); mTempTitleViewForCurrent = (TextView) mMenuView.findViewById(R.id.temp_title_for_current); } @@ -425,16 +474,21 @@ public class MenuLayoutManager { mPropertyValuesAfterAnimation.clear(); List<Animator> animators = new ArrayList<>(); boolean scrollDown = position > oldPosition; - List<Rect> layouts = getViewLayouts(mMenuView.getLeft(), mMenuView.getTop(), - mMenuView.getRight(), mMenuView.getBottom()); + List<Rect> layouts = + getViewLayouts( + mMenuView.getLeft(), + mMenuView.getTop(), + mMenuView.getRight(), + mMenuView.getBottom()); // Old row. MenuRow oldRow = mMenuRows.get(oldPosition); final MenuRowView oldView = mMenuRowViews.get(oldPosition); View oldContentsView = oldView.getContentsView(); // Old contents view. - animators.add(createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) - .setDuration(mOldContentsFadeOutDuration)); + animators.add( + createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) + .setDuration(mOldContentsFadeOutDuration)); final TextView oldTitleView = oldView.getTitleView(); setTempTitleView(mTempTitleViewForOld, oldTitleView); Rect oldLayoutRect = layouts.get(oldPosition); @@ -444,20 +498,36 @@ public class MenuLayoutManager { // This case is not included in the animation specification. mTempTitleViewForOld.setScaleX(1.0f); mTempTitleViewForOld.setScaleY(1.0f); - animators.add(createAlphaAnimator(mTempTitleViewForOld, 0.0f, - oldView.getTitleViewAlphaDeselected(), mFastOutLinearIn)); + animators.add( + createAlphaAnimator( + mTempTitleViewForOld, + 0.0f, + oldView.getTitleViewAlphaDeselected(), + mFastOutLinearIn)); int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); - animators.add(createTranslationYAnimator(mTempTitleViewForOld, - offset + mRowScrollUpAnimationOffset, offset)); + animators.add( + createTranslationYAnimator( + mTempTitleViewForOld, + offset + mRowScrollUpAnimationOffset, + offset)); } else { - animators.add(createScaleXAnimator(mTempTitleViewForOld, - oldView.getTitleViewScaleSelected(), 1.0f)); - animators.add(createScaleYAnimator(mTempTitleViewForOld, - oldView.getTitleViewScaleSelected(), 1.0f)); - animators.add(createAlphaAnimator(mTempTitleViewForOld, oldTitleView.getAlpha(), - oldView.getTitleViewAlphaDeselected(), mLinearOutSlowIn)); - animators.add(createTranslationYAnimator(mTempTitleViewForOld, 0, - oldLayoutRect.top - mTempTitleViewForOld.getTop())); + animators.add( + createScaleXAnimator( + mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add( + createScaleYAnimator( + mTempTitleViewForOld, oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add( + createAlphaAnimator( + mTempTitleViewForOld, + oldTitleView.getAlpha(), + oldView.getTitleViewAlphaDeselected(), + mLinearOutSlowIn)); + animators.add( + createTranslationYAnimator( + mTempTitleViewForOld, + 0, + oldLayoutRect.top - mTempTitleViewForOld.getTop())); } oldTitleView.setAlpha(oldView.getTitleViewAlphaDeselected()); oldTitleView.setVisibility(View.INVISIBLE); @@ -471,23 +541,30 @@ public class MenuLayoutManager { // The maximum is to the top of the start position of mTempTitleViewForOld. int distanceCurrentTitle = currentLayoutRect.top - currentView.getTop(); int distance = Math.max(mRowScrollUpAnimationOffset, distanceCurrentTitle); - int distanceToTopOfSecondTitle = oldLayoutRect.top - mRowScrollUpAnimationOffset - - oldView.getTop(); - animators.add(createTranslationYAnimator(oldTitleView, 0.0f, - Math.min(distance, distanceToTopOfSecondTitle))); - animators.add(createAlphaAnimator(oldTitleView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) - .setDuration(mOldContentsFadeOutDuration)); - animators.add(createScaleXAnimator(oldTitleView, - oldView.getTitleViewScaleSelected(), 1.0f)); - animators.add(createScaleYAnimator(oldTitleView, - oldView.getTitleViewScaleSelected(), 1.0f)); + int distanceToTopOfSecondTitle = + oldLayoutRect.top - mRowScrollUpAnimationOffset - oldView.getTop(); + animators.add( + createTranslationYAnimator( + oldTitleView, 0.0f, Math.min(distance, distanceToTopOfSecondTitle))); + animators.add( + createAlphaAnimator(oldTitleView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn) + .setDuration(mOldContentsFadeOutDuration)); + animators.add( + createScaleXAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f)); + animators.add( + createScaleYAnimator(oldTitleView, oldView.getTitleViewScaleSelected(), 1.0f)); mTempTitleViewForOld.setScaleX(1.0f); mTempTitleViewForOld.setScaleY(1.0f); - animators.add(createAlphaAnimator(mTempTitleViewForOld, 0.0f, - oldView.getTitleViewAlphaDeselected(), mFastOutLinearIn)); + animators.add( + createAlphaAnimator( + mTempTitleViewForOld, + 0.0f, + oldView.getTitleViewAlphaDeselected(), + mFastOutLinearIn)); int offset = oldLayoutRect.top - mTempTitleViewForOld.getTop(); - animators.add(createTranslationYAnimator(mTempTitleViewForOld, - offset - mRowScrollUpAnimationOffset, offset)); + animators.add( + createTranslationYAnimator( + mTempTitleViewForOld, offset - mRowScrollUpAnimationOffset, offset)); } // Current row. Rect currentLayoutRect = new Rect(layouts.get(position)); @@ -502,29 +579,40 @@ public class MenuLayoutManager { // The maximum is to the top of the end position of mTempTitleViewForCurrent. int distanceOldTitle = oldView.getTop() - oldLayoutRect.top; int distance = Math.max(mRowScrollUpAnimationOffset, distanceOldTitle); - int distanceTopOfSecondTitle = currentView.getTop() - mRowScrollUpAnimationOffset - - currentLayoutRect.top; - animators.add(createTranslationYAnimator(currentTitleView, - Math.min(distance, distanceTopOfSecondTitle), 0.0f)); + int distanceTopOfSecondTitle = + currentView.getTop() - mRowScrollUpAnimationOffset - currentLayoutRect.top; + animators.add( + createTranslationYAnimator( + currentTitleView, Math.min(distance, distanceTopOfSecondTitle), 0.0f)); currentView.setTop(currentLayoutRect.top); - ObjectAnimator animator = createAlphaAnimator(currentTitleView, 0.0f, 1.0f, - mFastOutLinearIn).setDuration(mCurrentContentsFadeInDuration); + ObjectAnimator animator = + createAlphaAnimator(currentTitleView, 0.0f, 1.0f, mFastOutLinearIn) + .setDuration(mCurrentContentsFadeInDuration); animator.setStartDelay(mOldContentsFadeOutDuration); currentTitleView.setAlpha(0.0f); animators.add(animator); - animators.add(createScaleXAnimator(currentTitleView, 1.0f, - currentView.getTitleViewScaleSelected())); - animators.add(createScaleYAnimator(currentTitleView, 1.0f, - currentView.getTitleViewScaleSelected())); - animators.add(createTranslationYAnimator(mTempTitleViewForCurrent, 0.0f, - -mRowScrollUpAnimationOffset)); - animators.add(createAlphaAnimator(mTempTitleViewForCurrent, - currentView.getTitleViewAlphaDeselected(), 0, mLinearOutSlowIn)); + animators.add( + createScaleXAnimator( + currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); + animators.add( + createScaleYAnimator( + currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); + animators.add( + createTranslationYAnimator( + mTempTitleViewForCurrent, 0.0f, -mRowScrollUpAnimationOffset)); + animators.add( + createAlphaAnimator( + mTempTitleViewForCurrent, + currentView.getTitleViewAlphaDeselected(), + 0, + mLinearOutSlowIn)); // Current contents view. - animators.add(createTranslationYAnimator(currentContentsView, - mRowScrollUpAnimationOffset, 0.0f)); - animator = createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) - .setDuration(mCurrentContentsFadeInDuration); + animators.add( + createTranslationYAnimator( + currentContentsView, mRowScrollUpAnimationOffset, 0.0f)); + animator = + createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) + .setDuration(mCurrentContentsFadeInDuration); animator.setStartDelay(mOldContentsFadeOutDuration); animators.add(animator); } else { @@ -532,17 +620,27 @@ public class MenuLayoutManager { // Current title view. int currentViewOffset = currentLayoutRect.top - currentView.getTop(); animators.add(createTranslationYAnimator(currentTitleView, 0, currentViewOffset)); - animators.add(createAlphaAnimator(currentTitleView, - currentView.getTitleViewAlphaDeselected(), 1.0f, mFastOutSlowIn)); - animators.add(createScaleXAnimator(currentTitleView, 1.0f, - currentView.getTitleViewScaleSelected())); - animators.add(createScaleYAnimator(currentTitleView, 1.0f, - currentView.getTitleViewScaleSelected())); + animators.add( + createAlphaAnimator( + currentTitleView, + currentView.getTitleViewAlphaDeselected(), + 1.0f, + mFastOutSlowIn)); + animators.add( + createScaleXAnimator( + currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); + animators.add( + createScaleYAnimator( + currentTitleView, 1.0f, currentView.getTitleViewScaleSelected())); // Current contents view. - animators.add(createTranslationYAnimator(currentContentsView, - currentViewOffset - mRowScrollUpAnimationOffset, currentViewOffset)); - ObjectAnimator animator = createAlphaAnimator(currentContentsView, 0.0f, 1.0f, - mFastOutLinearIn).setDuration(mCurrentContentsFadeInDuration); + animators.add( + createTranslationYAnimator( + currentContentsView, + currentViewOffset - mRowScrollUpAnimationOffset, + currentViewOffset)); + ObjectAnimator animator = + createAlphaAnimator(currentContentsView, 0.0f, 1.0f, mFastOutLinearIn) + .setDuration(mCurrentContentsFadeInDuration); animator.setStartDelay(mOldContentsFadeOutDuration); animators.add(animator); } @@ -553,9 +651,13 @@ public class MenuLayoutManager { if (nextPosition != INVALID_POSITION) { MenuRowView nextView = mMenuRowViews.get(nextPosition); Rect nextLayoutRect = layouts.get(nextPosition); - animators.add(createTranslationYAnimator(nextView, - nextLayoutRect.top + mRowScrollUpAnimationOffset - nextView.getTop(), - nextLayoutRect.top - nextView.getTop())); + animators.add( + createTranslationYAnimator( + nextView, + nextLayoutRect.top + + mRowScrollUpAnimationOffset + - nextView.getTop(), + nextLayoutRect.top - nextView.getTop())); animators.add(createAlphaAnimator(nextView, 0.0f, 1.0f, mFastOutLinearIn)); } } else { @@ -563,15 +665,22 @@ public class MenuLayoutManager { if (nextPosition != INVALID_POSITION) { MenuRowView nextView = mMenuRowViews.get(nextPosition); animators.add(createTranslationYAnimator(nextView, 0, mRowScrollUpAnimationOffset)); - animators.add(createAlphaAnimator(nextView, - nextView.getTitleViewAlphaDeselected(), 0.0f, 1.0f, mLinearOutSlowIn)); + animators.add( + createAlphaAnimator( + nextView, + nextView.getTitleViewAlphaDeselected(), + 0.0f, + 1.0f, + mLinearOutSlowIn)); } } // Other rows. int count = mMenuRowViews.size(); for (int i = 0; i < count; ++i) { MenuRowView view = mMenuRowViews.get(i); - if (view.getVisibility() == View.VISIBLE && i != oldPosition && i != position + if (view.getVisibility() == View.VISIBLE + && i != oldPosition + && i != position && i != nextPosition) { Rect rect = layouts.get(i); animators.add(createTranslationYAnimator(view, 0, rect.top - view.getTop())); @@ -582,51 +691,62 @@ public class MenuLayoutManager { propertyValuesAfterAnimation.addAll(mPropertyValuesAfterAnimation); mAnimatorSet = new AnimatorSet(); mAnimatorSet.playTogether(animators); - mAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - if (DEBUG) dumpChildren("onRowAnimationEndBefore"); - mAnimatorSet = null; - // The property values which are different from the end values and need to be - // changed after the animation are set here. - // e.g. setting translationY to 0, alpha of the contents view to 1. - for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { - holder.property.set(holder.view, holder.value); - } - oldView.onDeselected(); - currentView.onSelected(true); - mTempTitleViewForOld.setVisibility(View.GONE); - mTempTitleViewForCurrent.setVisibility(View.GONE); - layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(), - mMenuView.getBottom()); - if (DEBUG) dumpChildren("onRowAnimationEndAfter"); - - MenuRow currentRow = mMenuRows.get(position); - if (currentRow.hideTitleWhenSelected()) { - View titleView = mMenuRowViews.get(position).getTitleView(); - mTitleFadeOutAnimator = createAlphaAnimator(titleView, titleView.getAlpha(), - 0.0f, mLinearOutSlowIn); - mTitleFadeOutAnimator.setStartDelay(TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS); - mTitleFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - private boolean mCanceled; - - @Override - public void onAnimationCancel(Animator animator) { - mCanceled = true; + mAnimatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (DEBUG) dumpChildren("onRowAnimationEndBefore"); + mAnimatorSet = null; + // The property values which are different from the end values and need to + // be + // changed after the animation are set here. + // e.g. setting translationY to 0, alpha of the contents view to 1. + for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { + holder.property.set(holder.view, holder.value); } - - @Override - public void onAnimationEnd(Animator animator) { - mTitleFadeOutAnimator = null; - if (!mCanceled) { - mMenuRowViews.get(position).onSelected(false); - } + oldView.onDeselected(); + currentView.onSelected(true); + mTempTitleViewForOld.setVisibility(View.GONE); + mTempTitleViewForCurrent.setVisibility(View.GONE); + layout( + mMenuView.getLeft(), + mMenuView.getTop(), + mMenuView.getRight(), + mMenuView.getBottom()); + if (DEBUG) dumpChildren("onRowAnimationEndAfter"); + + MenuRow currentRow = mMenuRows.get(position); + if (currentRow.hideTitleWhenSelected()) { + View titleView = mMenuRowViews.get(position).getTitleView(); + mTitleFadeOutAnimator = + createAlphaAnimator( + titleView, + titleView.getAlpha(), + 0.0f, + mLinearOutSlowIn); + mTitleFadeOutAnimator.setStartDelay( + TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS); + mTitleFadeOutAnimator.addListener( + new AnimatorListenerAdapter() { + private boolean mCanceled; + + @Override + public void onAnimationCancel(Animator animator) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + mTitleFadeOutAnimator = null; + if (!mCanceled) { + mMenuRowViews.get(position).onSelected(false); + } + } + }); + mTitleFadeOutAnimator.start(); } - }); - mTitleFadeOutAnimator.start(); - } - } - }); + } + }); mAnimatorSet.start(); if (DEBUG) dumpChildren("startedRowAnimation()"); } @@ -661,7 +781,8 @@ public class MenuLayoutManager { if (mMenuView.getVisibility() != View.VISIBLE) { int count = mMenuRowViews.size(); for (int i = 0; i < count; ++i) { - mMenuRowViews.get(i) + mMenuRowViews + .get(i) .setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE : View.GONE); } return; @@ -674,8 +795,8 @@ public class MenuLayoutManager { for (int i = mSelectedPosition - 1; i >= 0; --i) { MenuRow row = mMenuRows.get(i); MenuRowView view = mMenuRowViews.get(i); - if (row.isVisible() && (view.getVisibility() == View.GONE - || mRemovingRowViews.contains(i))) { + if (row.isVisible() + && (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) { // Removing rows are still VISIBLE. addedRowViews.add(i); ++added; @@ -691,8 +812,8 @@ public class MenuLayoutManager { for (int i = mSelectedPosition + 1; i < count; ++i) { MenuRow row = mMenuRows.get(i); MenuRowView view = mMenuRowViews.get(i); - if (row.isVisible() && (view.getVisibility() == View.GONE - || mRemovingRowViews.contains(i))) { + if (row.isVisible() + && (view.getVisibility() == View.GONE || mRemovingRowViews.contains(i))) { // Removing rows are still VISIBLE. addedRowViews.add(i); ++added; @@ -717,8 +838,14 @@ public class MenuLayoutManager { } mPropertyValuesAfterAnimation.clear(); List<Animator> animators = new ArrayList<>(); - List<Rect> layouts = getViewLayouts(mMenuView.getLeft(), mMenuView.getTop(), - mMenuView.getRight(), mMenuView.getBottom(), addedRowViews, removedRowViews); + List<Rect> layouts = + getViewLayouts( + mMenuView.getLeft(), + mMenuView.getTop(), + mMenuView.getRight(), + mMenuView.getBottom(), + addedRowViews, + removedRowViews); for (int position : addedRowViews) { MenuRowView view = mMenuRowViews.get(position); view.setVisibility(View.VISIBLE); @@ -728,7 +855,8 @@ public class MenuLayoutManager { view.layout(rect.left, rect.top, rect.right, rect.bottom); View titleView = view.getTitleView(); MarginLayoutParams params = (MarginLayoutParams) titleView.getLayoutParams(); - titleView.layout(view.getPaddingLeft() + params.leftMargin, + titleView.layout( + view.getPaddingLeft() + params.leftMargin, view.getPaddingTop() + params.topMargin, rect.right - rect.left - view.getPaddingRight() - params.rightMargin, rect.bottom - rect.top - view.getPaddingBottom() - params.bottomMargin); @@ -749,23 +877,28 @@ public class MenuLayoutManager { mRemovingRowViews.addAll(removedRowViews); mAnimatorSet = new AnimatorSet(); mAnimatorSet.playTogether(animators); - mAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimatorSet = null; - // The property values which are different from the end values and need to be - // changed after the animation are set here. - // e.g. setting translationY to 0, alpha of the contents view to 1. - for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { - holder.property.set(holder.view, holder.value); - } - for (int position : mRemovingRowViews) { - mMenuRowViews.get(position).setVisibility(View.GONE); - } - layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(), - mMenuView.getBottom()); - } - }); + mAnimatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimatorSet = null; + // The property values which are different from the end values and need to + // be + // changed after the animation are set here. + // e.g. setting translationY to 0, alpha of the contents view to 1. + for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) { + holder.property.set(holder.view, holder.value); + } + for (int position : mRemovingRowViews) { + mMenuRowViews.get(position).setVisibility(View.GONE); + } + layout( + mMenuView.getLeft(), + mMenuView.getTop(), + mMenuView.getRight(), + mMenuView.getBottom()); + } + }); mAnimatorSet.start(); if (DEBUG) dumpChildren("onMenuRowUpdated()"); } @@ -778,16 +911,16 @@ public class MenuLayoutManager { return animator; } - private ObjectAnimator createAlphaAnimator(View view, float from, float to, - TimeInterpolator interpolator) { + private ObjectAnimator createAlphaAnimator( + View view, float from, float to, TimeInterpolator interpolator) { ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); animator.setDuration(mRowAnimationDuration); animator.setInterpolator(interpolator); return animator; } - private ObjectAnimator createAlphaAnimator(View view, float from, float to, float end, - TimeInterpolator interpolator) { + private ObjectAnimator createAlphaAnimator( + View view, float from, float to, float end, TimeInterpolator interpolator) { ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, from, to); animator.setDuration(mRowAnimationDuration); animator.setInterpolator(interpolator); @@ -809,9 +942,7 @@ public class MenuLayoutManager { return animator; } - /** - * Returns the current position. - */ + /** Returns the current position. */ public int getSelectedPosition() { return mSelectedPosition; } @@ -828,15 +959,10 @@ public class MenuLayoutManager { } } - /** - * Called when the menu becomes visible. - */ - public void onMenuShow() { - } + /** Called when the menu becomes visible. */ + public void onMenuShow() {} - /** - * Called when the menu becomes hidden. - */ + /** Called when the menu becomes hidden. */ public void onMenuHide() { if (mAnimatorSet != null) { mAnimatorSet.end(); diff --git a/src/com/android/tv/menu/MenuRow.java b/src/com/android/tv/menu/MenuRow.java index 47804f11..8dc12bad 100644 --- a/src/com/android/tv/menu/MenuRow.java +++ b/src/com/android/tv/menu/MenuRow.java @@ -17,13 +17,11 @@ package com.android.tv.menu; import android.content.Context; -import android.view.View; /** - * A base class of the item which will be displayed in the main menu. - * It contains the data such as title to represent a row. - * This is an abstract class and the sub-class could have it's own data for - * the row. + * A base class of the item which will be displayed in the main menu. It contains the data such as + * title to represent a row. This is an abstract class and the sub-class could have it's own data + * for the row. */ public abstract class MenuRow { private final Context mContext; @@ -45,86 +43,60 @@ public abstract class MenuRow { mHeight = context.getResources().getDimensionPixelSize(heightResId); } - /** - * Returns the context. - */ + /** Returns the context. */ protected Context getContext() { return mContext; } - /** - * Returns the menu object. - */ + /** Returns the menu object. */ public Menu getMenu() { return mMenu; } - /** - * Returns the title of this row. - */ + /** Returns the title of this row. */ public String getTitle() { return mTitle; } - /** - * Returns the height of this row. - */ + /** Returns the height of this row. */ public int getHeight() { return mHeight; } - /** - * Sets the menu row view. - */ + /** Sets the menu row view. */ public void setMenuRowView(MenuRowView menuRowView) { mMenuRowView = menuRowView; } - /** - * Returns the menu row view. - */ + /** Returns the menu row view. */ protected MenuRowView getMenuRowView() { return mMenuRowView; } - /** - * Updates the contents in this row. - * This method is called only by the menu when necessary. - */ - abstract public void update(); + /** Updates the contents in this row. This method is called only by the menu when necessary. */ + public abstract void update(); - /** - * Indicates whether this row is shown in the menu. - */ + /** Indicates whether this row is shown in the menu. */ public boolean isVisible() { return true; } /** - * Releases all the resources which need to be released. - * This method is called when the main menu is not available any more. + * Releases all the resources which need to be released. This method is called when the main + * menu is not available any more. */ - public void release() { - } + public void release() {} - /** - * Returns the ID of the layout resource for this row. - */ - abstract public int getLayoutResId(); + /** Returns the ID of the layout resource for this row. */ + public abstract int getLayoutResId(); - /** - * Returns the ID of this row. This ID is used to select the row in the main menu. - */ - abstract public String getId(); + /** Returns the ID of this row. This ID is used to select the row in the main menu. */ + public abstract String getId(); - /** - * This method is called when recent channels are changed. - */ - public void onRecentChannelsChanged() { } + /** This method is called when recent channels are changed. */ + public void onRecentChannelsChanged() {} - /** - * Returns whether to hide the title when the row is selected. - */ + /** Returns whether to hide the title when the row is selected. */ public boolean hideTitleWhenSelected() { return false; } diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java index 570cfb8f..048d725d 100644 --- a/src/com/android/tv/menu/MenuRowFactory.java +++ b/src/com/android/tv/menu/MenuRowFactory.java @@ -19,80 +19,76 @@ package com.android.tv.menu; import android.content.Context; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.customization.CustomAction; -import com.android.tv.customization.TvCustomizationManager; +import com.android.tv.common.customization.CustomAction; +import com.android.tv.common.customization.CustomizationManager; import com.android.tv.ui.TunableTvView; - import java.util.List; -/** - * A factory class to create menu rows. - */ +/** A factory class to create menu rows. */ public class MenuRowFactory { private final MainActivity mMainActivity; private final TunableTvView mTvView; - private final TvCustomizationManager mTvCustomizationManager; + private final CustomizationManager mCustomizationManager; - /** - * A constructor. - */ + /** A constructor. */ public MenuRowFactory(MainActivity mainActivity, TunableTvView tvView) { mMainActivity = mainActivity; mTvView = tvView; - mTvCustomizationManager = new TvCustomizationManager(mainActivity); - mTvCustomizationManager.initialize(); + mCustomizationManager = new CustomizationManager(mainActivity); + mCustomizationManager.initialize(); } - /** - * Creates an object corresponding to the given {@code key}. - */ + /** Creates an object corresponding to the given {@code key}. */ @Nullable public MenuRow createMenuRow(Menu menu, Class<?> key) { if (PlayControlsRow.class.equals(key)) { - return new PlayControlsRow(mMainActivity, mTvView, menu, - mMainActivity.getTimeShiftManager()); + return new PlayControlsRow( + mMainActivity, mTvView, menu, mMainActivity.getTimeShiftManager()); } else if (ChannelsRow.class.equals(key)) { return new ChannelsRow(mMainActivity, menu, mMainActivity.getProgramDataManager()); } else if (PartnerRow.class.equals(key)) { - List<CustomAction> customActions = mTvCustomizationManager.getCustomActions( - TvCustomizationManager.ID_PARTNER_ROW); - String title = mTvCustomizationManager.getPartnerRowTitle(); + List<CustomAction> customActions = + mCustomizationManager.getCustomActions(CustomizationManager.ID_PARTNER_ROW); + String title = mCustomizationManager.getPartnerRowTitle(); if (customActions != null && !TextUtils.isEmpty(title)) { return new PartnerRow(mMainActivity, menu, title, customActions); } return null; } else if (TvOptionsRow.class.equals(key)) { - return new TvOptionsRow(mMainActivity, menu, mTvCustomizationManager - .getCustomActions(TvCustomizationManager.ID_OPTIONS_ROW)); + return new TvOptionsRow( + mMainActivity, + menu, + mCustomizationManager.getCustomActions(CustomizationManager.ID_OPTIONS_ROW)); } return null; } - /** - * A menu row which represents the TV options row. - */ + /** A menu row which represents the TV options row. */ public static class TvOptionsRow extends ItemListRow { - /** - * The ID of the row. - */ + /** The ID of the row. */ public static final String ID = TvOptionsRow.class.getName(); private TvOptionsRow(Context context, Menu menu, List<CustomAction> customActions) { - super(context, menu, R.string.menu_title_options, R.dimen.action_card_height, + super( + context, + menu, + R.string.menu_title_options, + R.dimen.action_card_height, new TvOptionsRowAdapter(context, customActions)); } } - /** - * A menu row which represents the partner row. - */ + /** A menu row which represents the partner row. */ public static class PartnerRow extends ItemListRow { - private PartnerRow(Context context, Menu menu, String title, - List<CustomAction> customActions) { - super(context, menu, title, R.dimen.action_card_height, + private PartnerRow( + Context context, Menu menu, String title, List<CustomAction> customActions) { + super( + context, + menu, + title, + R.dimen.action_card_height, new PartnerOptionsRowAdapter(context, customActions)); } } diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java index 97dea29a..a064f352 100644 --- a/src/com/android/tv/menu/MenuRowView.java +++ b/src/com/android/tv/menu/MenuRowView.java @@ -27,7 +27,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.menu.Menu.MenuShowReason; @@ -46,25 +45,23 @@ public abstract class MenuRowView extends LinearLayout { * reset when the menu is popped up. */ private View mLastFocusView; + private MenuRow mRow; - private final OnFocusChangeListener mOnFocusChangeListener = new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - onChildFocusChange(v, hasFocus); - } - }; + private final OnFocusChangeListener mOnFocusChangeListener = + new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + onChildFocusChange(v, hasFocus); + } + }; - /** - * Returns the alpha value of the title view when it's deselected. - */ + /** Returns the alpha value of the title view when it's deselected. */ public float getTitleViewAlphaDeselected() { return mTitleViewAlphaDeselected; } - /** - * Returns the scale value of the title view when it's selected. - */ + /** Returns the scale value of the title view when it's selected. */ public float getTitleViewScaleSelected() { return mTitleViewScaleSelected; } @@ -125,26 +122,22 @@ public abstract class MenuRowView extends LinearLayout { } } - abstract protected int getContentsViewId(); + protected abstract int getContentsViewId(); - /** - * Returns the title view. - */ + /** Returns the title view. */ public final TextView getTitleView() { return mTitleView; } - /** - * Returns the contents view. - */ + /** Returns the contents view. */ public final View getContentsView() { return mContentsView; } /** - * Initialize this view. e.g. Set the initial selection. - * This method is called when the main menu is visible. - * Subclass of {@link MenuRowView} should override this to set correct mLastFocusView. + * Initialize this view. e.g. Set the initial selection. This method is called when the main + * menu is visible. Subclass of {@link MenuRowView} should override this to set correct + * mLastFocusView. * * @param reason A reason why this is initialized. See {@link MenuShowReason} */ @@ -177,17 +170,17 @@ public abstract class MenuRowView extends LinearLayout { } /** - * Sets the view which needs to have focus when this row appears. - * Subclasses should call this in {@link #initialize} if needed. + * Sets the view which needs to have focus when this row appears. Subclasses should call this in + * {@link #initialize} if needed. */ protected void setInitialFocusView(@NonNull View v) { mLastFocusView = v; } /** - * Called when the focus of a child view is changed. - * The inherited class should override this method instead of calling - * {@link android.view.View#setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}. + * Called when the focus of a child view is changed. The inherited class should override this + * method instead of calling {@link + * android.view.View#setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}. */ protected void onChildFocusChange(View v, boolean hasFocus) { if (hasFocus) { @@ -195,9 +188,7 @@ public abstract class MenuRowView extends LinearLayout { } } - /** - * Returns the ID of row object bound to this view. - */ + /** Returns the ID of row object bound to this view. */ public String getRowId() { return mRow == null ? null : mRow.getId(); } @@ -206,7 +197,7 @@ public abstract class MenuRowView extends LinearLayout { * Called when this row is selected. * * @param showTitle If {@code true}, the title is not hidden immediately after the row is - * selected even though hideTitleWhenSelected() is {@code true}. + * selected even though hideTitleWhenSelected() is {@code true}. */ public void onSelected(boolean showTitle) { if (mRow.hideTitleWhenSelected() && !showTitle) { @@ -225,9 +216,7 @@ public abstract class MenuRowView extends LinearLayout { mLastFocusView = lastFocusView; } - /** - * Called when this row is deselected. - */ + /** Called when this row is deselected. */ public void onDeselected() { mTitleView.setVisibility(VISIBLE); mTitleView.setAlpha(mTitleViewAlphaDeselected); @@ -236,9 +225,7 @@ public abstract class MenuRowView extends LinearLayout { mContentsView.setVisibility(GONE); } - /** - * Returns the preferred height of the contents view. The top/bottom padding is excluded. - */ + /** Returns the preferred height of the contents view. The top/bottom padding is excluded. */ public int getPreferredContentsHeight() { return mRow.getHeight(); } diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java index 18416c85..5d277824 100644 --- a/src/com/android/tv/menu/MenuUpdater.java +++ b/src/com/android/tv/menu/MenuUpdater.java @@ -17,12 +17,11 @@ package com.android.tv.menu; import android.support.annotation.Nullable; - import com.android.tv.ChannelTuner; import com.android.tv.TvOptionsManager; import com.android.tv.TvOptionsManager.OptionChangedListener; import com.android.tv.TvOptionsManager.OptionType; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.menu.MenuRowFactory.TvOptionsRow; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; @@ -39,49 +38,52 @@ public class MenuUpdater { @Nullable private final TvOptionsManager mOptionsManager; private ChannelTuner mChannelTuner; - private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { - @Override - public void onLoadFinished() {} + private final ChannelTuner.Listener mChannelTunerListener = + new ChannelTuner.Listener() { + @Override + public void onLoadFinished() {} - @Override - public void onBrowsableChannelListChanged() { - mMenu.update(ChannelsRow.ID); - } + @Override + public void onBrowsableChannelListChanged() { + mMenu.update(ChannelsRow.ID); + } - @Override - public void onCurrentChannelUnavailable(Channel channel) {} + @Override + public void onCurrentChannelUnavailable(Channel channel) {} - @Override - public void onChannelChanged(Channel previousChannel, Channel currentChannel) { - mMenu.update(ChannelsRow.ID); - } - }; - private final OptionChangedListener mOptionChangeListener = new OptionChangedListener() { - @Override - public void onOptionChanged(@OptionType int optionType, String newString) { - mMenu.update(TvOptionsRow.ID); - } - }; + @Override + public void onChannelChanged(Channel previousChannel, Channel currentChannel) { + mMenu.update(ChannelsRow.ID); + } + }; + private final OptionChangedListener mOptionChangeListener = + new OptionChangedListener() { + @Override + public void onOptionChanged(@OptionType int optionType, String newString) { + mMenu.update(TvOptionsRow.ID); + } + }; public MenuUpdater(Menu menu, TunableTvView tvView, TvOptionsManager optionsManager) { mMenu = menu; mTvView = tvView; mOptionsManager = optionsManager; if (mTvView != null) { - mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() { - @Override - public void onScreenBlockingChanged(boolean blocked) { - mMenu.update(PlayControlsRow.ID); - } - }); + mTvView.setOnScreenBlockedListener( + new OnScreenBlockingChangedListener() { + @Override + public void onScreenBlockingChanged(boolean blocked) { + mMenu.update(PlayControlsRow.ID); + } + }); } if (mOptionsManager != null) { - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, - mOptionChangeListener); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, - mOptionChangeListener); - mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, - mOptionChangeListener); + mOptionsManager.setOptionChangedListener( + TvOptionsManager.OPTION_CLOSED_CAPTIONS, mOptionChangeListener); + mOptionsManager.setOptionChangedListener( + TvOptionsManager.OPTION_DISPLAY_MODE, mOptionChangeListener); + mOptionsManager.setOptionChangedListener( + TvOptionsManager.OPTION_MULTI_AUDIO, mOptionChangeListener); } } @@ -98,16 +100,12 @@ public class MenuUpdater { } } - /** - * Called when the stream information changes. - */ + /** Called when the stream information changes. */ public void onStreamInfoChanged() { mMenu.update(TvOptionsRow.ID); } - /** - * Called at the end of the menu's lifetime. - */ + /** Called at the end of the menu's lifetime. */ public void release() { if (mChannelTuner != null) { mChannelTuner.removeListener(mChannelTunerListener); diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java index ee0b036e..f5fec000 100644 --- a/src/com/android/tv/menu/MenuView.java +++ b/src/com/android/tv/menu/MenuView.java @@ -26,15 +26,11 @@ import android.view.ViewParent; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.FrameLayout; - import com.android.tv.menu.Menu.MenuShowReason; - import java.util.ArrayList; import java.util.List; -/** - * A view that represents TV main menu. - */ +/** A view that represents TV main menu. */ public class MenuView extends FrameLayout implements IMenuView { static final String TAG = MenuView.class.getSimpleName(); static final boolean DEBUG = false; @@ -60,20 +56,25 @@ public class MenuView extends FrameLayout implements IMenuView { mLayoutInflater = LayoutInflater.from(context); // Set hardware layer type for smooth animation of lots of views. setLayerType(LAYER_TYPE_HARDWARE, null); - getViewTreeObserver().addOnGlobalFocusChangeListener(new OnGlobalFocusChangeListener() { - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - MenuRowView newParent = getParentMenuRowView(newFocus); - if (newParent != null) { - if (DEBUG) Log.d(TAG, "Focus changed to " + newParent); - // When the row is selected, the row view itself has the focus because the row - // is collapsed. To make the child of the row have the focus, requestFocus() - // should be called again after the row is expanded. It's done in - // setSelectedPosition(). - setSelectedPositionSmooth(mMenuRowViews.indexOf(newParent)); - } - } - }); + getViewTreeObserver() + .addOnGlobalFocusChangeListener( + new OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + MenuRowView newParent = getParentMenuRowView(newFocus); + if (newParent != null) { + if (DEBUG) Log.d(TAG, "Focus changed to " + newParent); + // When the row is selected, the row view itself has the focus + // because the row + // is collapsed. To make the child of the row have the focus, + // requestFocus() + // should be called again after the row is expanded. It's done + // in + // setSelectedPosition(). + setSelectedPositionSmooth(mMenuRowViews.indexOf(newParent)); + } + } + }); mLayoutManager = new MenuLayoutManager(context, this); } @@ -102,8 +103,8 @@ public class MenuView extends FrameLayout implements IMenuView { } @Override - public void onShow(@MenuShowReason int reason, String rowIdToSelect, - final Runnable runnableAfterShow) { + public void onShow( + @MenuShowReason int reason, String rowIdToSelect, final Runnable runnableAfterShow) { if (DEBUG) { Log.d(TAG, "onShow(reason=" + reason + ", rowIdToSelect=" + rowIdToSelect + ")"); } @@ -132,15 +133,18 @@ public class MenuView extends FrameLayout implements IMenuView { // Make the selected row have the focus. requestFocus(); if (runnableAfterShow != null) { - getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getViewTreeObserver().removeOnGlobalLayoutListener(this); - // Start show animation after layout finishes for smooth animation because the - // layout can take long time. - runnableAfterShow.run(); - } - }); + getViewTreeObserver() + .addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + // Start show animation after layout finishes for smooth + // animation because the + // layout can take long time. + runnableAfterShow.run(); + } + }); } mLayoutManager.onMenuShow(); } diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java index dd6194a1..ceffe861 100644 --- a/src/com/android/tv/menu/OptionsRowAdapter.java +++ b/src/com/android/tv/menu/OptionsRowAdapter.java @@ -18,11 +18,9 @@ package com.android.tv.menu; import android.content.Context; import android.view.View; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; - import java.util.List; /* @@ -33,33 +31,33 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< protected final Tracker mTracker; private List<MenuAction> mActionList; - private final View.OnClickListener mMenuActionOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - final MenuAction action = (MenuAction) view.getTag(); - view.post(new Runnable() { + private final View.OnClickListener mMenuActionOnClickListener = + new View.OnClickListener() { @Override - public void run() { - int resId = action.getActionNameResId(); - if (resId == 0) { - mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL); - } else { - mTracker.sendMenuClicked(resId); - } - executeAction(action.getType()); + public void onClick(View view) { + final MenuAction action = (MenuAction) view.getTag(); + view.post( + new Runnable() { + @Override + public void run() { + int resId = action.getActionNameResId(); + if (resId == 0) { + mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL); + } else { + mTracker.sendMenuClicked(resId); + } + executeAction(action.getType()); + } + }); } - }); - } - }; + }; public OptionsRowAdapter(Context context) { super(context); - mTracker = TvApplication.getSingletons(context).getTracker(); + mTracker = TvSingletons.getSingletons(context).getTracker(); } - /** - * Update action list and its content. - */ + /** Update action list and its content. */ @Override public void update() { if (mActionList == null) { @@ -76,13 +74,14 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter< } protected abstract List<MenuAction> createActions(); + protected abstract void updateActions(); + protected abstract void executeAction(int type); /** - * Gets the action at the given position. - * Note that action at the position may differ from returned by {@link #createActions}. - * See {@link CustomizableOptionsRowAdapter} + * Gets the action at the given position. Note that action at the position may differ from + * returned by {@link #createActions}. See {@link CustomizableOptionsRowAdapter} */ protected MenuAction getAction(int position) { return mActionList.get(position); diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java index c8249a4c..9676fe4d 100644 --- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java +++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java @@ -17,9 +17,7 @@ package com.android.tv.menu; import android.content.Context; - -import com.android.tv.customization.CustomAction; - +import com.android.tv.common.customization.CustomAction; import java.util.Collections; import java.util.List; @@ -34,8 +32,7 @@ public class PartnerOptionsRowAdapter extends CustomizableOptionsRowAdapter { } @Override - protected void executeBaseAction(int option) { - } + protected void executeBaseAction(int option) {} @Override protected void updateActions() { diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java index 77715f28..ac3292a3 100644 --- a/src/com/android/tv/menu/PlayControlsButton.java +++ b/src/com/android/tv/menu/PlayControlsButton.java @@ -25,7 +25,6 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - import com.android.tv.R; public class PlayControlsButton extends FrameLayout { @@ -54,8 +53,8 @@ public class PlayControlsButton extends FrameLayout { this(context, attrs, defStyleAttr, 0); } - public PlayControlsButton(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + public PlayControlsButton( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); inflate(context, R.layout.play_controls_button, this); mButton = (ImageView) findViewById(R.id.button); @@ -66,9 +65,7 @@ public class PlayControlsButton extends FrameLayout { mIconFocusedColor = mIconColor; } - /** - * Sets the resource ID of the image to be displayed in the center of this control. - */ + /** Sets the resource ID of the image to be displayed in the center of this control. */ public void setImageResId(int imageResId) { int newTintColor = hasFocus() ? mIconFocusedColor : mIconColor; if (mImageResourceId != imageResId) { @@ -87,40 +84,39 @@ public class PlayControlsButton extends FrameLayout { mIcon.getDrawable().setTint(tintColor); } - /** - * Sets an action which is to be run when the button is clicked. - */ + /** Sets an action which is to be run when the button is clicked. */ public void setAction(final Runnable clickAction) { - mButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - clickAction.run(); - } - }); + mButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + clickAction.run(); + } + }); } - /** - * Sets the icon's color should change to when the button is on focus. - */ + /** Sets the icon's color should change to when the button is on focus. */ public void setFocusedIconColor(int color) { final ValueAnimator valueAnimator = ValueAnimator.ofArgb(mIconColor, color); - valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(final ValueAnimator animator) { - mIcon.getDrawable().setTint((int) animator.getAnimatedValue()); - } - }); + valueAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(final ValueAnimator animator) { + mIcon.getDrawable().setTint((int) animator.getAnimatedValue()); + } + }); valueAnimator.setDuration(mFocusAnimationTimeMs); - mButton.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - valueAnimator.start(); - } else { - valueAnimator.reverse(); - } - } - }); + mButton.setOnFocusChangeListener( + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + valueAnimator.start(); + } else { + valueAnimator.reverse(); + } + } + }); mIconFocusedColor = color; } diff --git a/src/com/android/tv/menu/PlayControlsRow.java b/src/com/android/tv/menu/PlayControlsRow.java index a60ff153..f19bd2b0 100644 --- a/src/com/android/tv/menu/PlayControlsRow.java +++ b/src/com/android/tv/menu/PlayControlsRow.java @@ -17,7 +17,6 @@ package com.android.tv.menu; import android.content.Context; - import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.ui.TunableTvView; @@ -28,8 +27,8 @@ public class PlayControlsRow extends MenuRow { private final TunableTvView mTvView; private final TimeShiftManager mTimeShiftManager; - public PlayControlsRow(Context context, TunableTvView tvView, Menu menu, - TimeShiftManager timeShiftManager) { + public PlayControlsRow( + Context context, TunableTvView tvView, Menu menu, TimeShiftManager timeShiftManager) { super(context, menu, R.string.menu_title_play_controls, R.dimen.play_controls_height); mTvView = tvView; mTimeShiftManager = timeShiftManager; @@ -45,16 +44,12 @@ public class PlayControlsRow extends MenuRow { return R.layout.play_controls; } - /** - * Returns TV view. - */ + /** Returns TV view. */ public TunableTvView getTvView() { return mTvView; } - /** - * Returns an instance of {@link TimeShiftManager}. - */ + /** Returns an instance of {@link TimeShiftManager}. */ public TimeShiftManager getTimeShiftManager() { return mTimeShiftManager; } diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index 4d766788..496d1969 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -24,16 +24,15 @@ import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import android.widget.Toast; - import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TimeShiftManager.TimeShiftActionId; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; @@ -79,27 +78,29 @@ public class PlayControlsRowView extends MenuRowView { private final String mUnavailableMessage; - private final ScheduledRecordingListener mScheduledRecordingListener - = new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - Channel currentChannel = mMainActivity.getCurrentChannel(); - if (currentChannel != null && isShown()) { - for (ScheduledRecording schedule : scheduledRecordings) { - if (schedule.getChannelId() == currentChannel.getId()) { - updateRecordButton(); - break; + private final ScheduledRecordingListener mScheduledRecordingListener = + new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {} + + @Override + public void onScheduledRecordingRemoved( + ScheduledRecording... scheduledRecordings) {} + + @Override + public void onScheduledRecordingStatusChanged( + ScheduledRecording... scheduledRecordings) { + Channel currentChannel = mMainActivity.getCurrentChannel(); + if (currentChannel != null && isShown()) { + for (ScheduledRecording schedule : scheduledRecordings) { + if (schedule.getChannelId() == currentChannel.getId()) { + updateRecordButton(); + break; + } + } } } - } - } - }; + }; public PlayControlsRowView(Context context) { this(context, null); @@ -113,22 +114,21 @@ public class PlayControlsRowView extends MenuRowView { this(context, attrs, defStyleAttr, 0); } - public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + public PlayControlsRowView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Resources res = context.getResources(); mTimeIndicatorLeftMargin = - - res.getDimensionPixelSize(R.dimen.play_controls_time_indicator_width) / 2; - mTimeTextLeftMargin = - - res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2; + -res.getDimensionPixelSize(R.dimen.play_controls_time_indicator_width) / 2; + mTimeTextLeftMargin = -res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2; mTimelineWidth = res.getDimensionPixelSize(R.dimen.play_controls_width); mTimeFormat = DateFormat.getTimeFormat(context); mNormalButtonMargin = res.getDimensionPixelSize(R.dimen.play_controls_button_normal_margin); mCompactButtonMargin = res.getDimensionPixelSize(R.dimen.play_controls_button_compact_margin); if (CommonFeatures.DVR.isEnabled(context)) { - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); + mDvrManager = TvSingletons.getSingletons(context).getDvrManager(); } else { mDvrDataManager = null; mDvrManager = null; @@ -154,7 +154,6 @@ public class PlayControlsRowView extends MenuRowView { } }); } - } } @@ -181,104 +180,141 @@ public class PlayControlsRowView extends MenuRowView { mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time); mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time); - initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous, - R.string.play_controls_description_skip_previous, null, new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.jumpToPrevious(); - updateControls(true); - } - } - }); - initializeButton(mRewindButton, R.drawable.lb_ic_fast_rewind, - R.string.play_controls_description_fast_rewind, null, new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.rewind(); - updateButtons(); - } - } - }); - initializeButton(mPlayPauseButton, R.drawable.lb_ic_play, - R.string.play_controls_description_play_pause, null, new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.togglePlayPause(); - updateButtons(); - } - } - }); - initializeButton(mFastForwardButton, R.drawable.lb_ic_fast_forward, - R.string.play_controls_description_fast_forward, null, new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.fastForward(); - updateButtons(); - } - } - }); - initializeButton(mJumpNextButton, R.drawable.lb_ic_skip_next, - R.string.play_controls_description_skip_next, null, new Runnable() { - @Override - public void run() { - if (mTimeShiftManager.isAvailable()) { - mTimeShiftManager.jumpToNext(); - updateControls(true); - } - } - }); - int color = getResources().getColor(R.color.play_controls_recording_icon_color_on_focus, - null); - initializeButton(mRecordButton, R.drawable.ic_record_start, R.string - .channels_item_record_start, color, new Runnable() { - @Override - public void run() { - onRecordButtonClicked(); - } - }); + initializeButton( + mJumpPreviousButton, + R.drawable.lb_ic_skip_previous, + R.string.play_controls_description_skip_previous, + null, + new Runnable() { + @Override + public void run() { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.jumpToPrevious(); + updateControls(true); + } + } + }); + initializeButton( + mRewindButton, + R.drawable.lb_ic_fast_rewind, + R.string.play_controls_description_fast_rewind, + null, + new Runnable() { + @Override + public void run() { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.rewind(); + updateButtons(); + } + } + }); + initializeButton( + mPlayPauseButton, + R.drawable.lb_ic_play, + R.string.play_controls_description_play_pause, + null, + new Runnable() { + @Override + public void run() { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.togglePlayPause(); + updateButtons(); + } + } + }); + initializeButton( + mFastForwardButton, + R.drawable.lb_ic_fast_forward, + R.string.play_controls_description_fast_forward, + null, + new Runnable() { + @Override + public void run() { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.fastForward(); + updateButtons(); + } + } + }); + initializeButton( + mJumpNextButton, + R.drawable.lb_ic_skip_next, + R.string.play_controls_description_skip_next, + null, + new Runnable() { + @Override + public void run() { + if (mTimeShiftManager.isAvailable()) { + mTimeShiftManager.jumpToNext(); + updateControls(true); + } + } + }); + int color = + getResources().getColor(R.color.play_controls_recording_icon_color_on_focus, null); + initializeButton( + mRecordButton, + R.drawable.ic_record_start, + R.string.channels_item_record_start, + color, + new Runnable() { + @Override + public void run() { + onRecordButtonClicked(); + } + }); } private boolean isCurrentChannelRecording() { Channel currentChannel = mMainActivity.getCurrentChannel(); - return currentChannel != null && mDvrManager != null + return currentChannel != null + && mDvrManager != null && mDvrManager.getCurrentRecording(currentChannel.getId()) != null; } private void onRecordButtonClicked() { boolean isRecording = isCurrentChannelRecording(); Channel currentChannel = mMainActivity.getCurrentChannel(); - TvApplication.getSingletons(getContext()).getTracker().sendMenuClicked(isRecording ? - R.string.channels_item_record_start : R.string.channels_item_record_stop); + TvSingletons.getSingletons(getContext()) + .getTracker() + .sendMenuClicked( + isRecording + ? R.string.channels_item_record_start + : R.string.channels_item_record_stop); if (!isRecording) { if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) { - Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel, - Toast.LENGTH_SHORT).show(); + Toast.makeText( + mMainActivity, + R.string.dvr_msg_cannot_record_channel, + Toast.LENGTH_SHORT) + .show(); } else { - Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager() - .getCurrentProgram(currentChannel.getId()); - DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, - currentChannel.getInputId(), new Runnable() { + Program program = + TvSingletons.getSingletons(mMainActivity) + .getProgramDataManager() + .getCurrentProgram(currentChannel.getId()); + DvrUiHelper.checkStorageStatusAndShowErrorMessage( + mMainActivity, + currentChannel.getInputId(), + new Runnable() { @Override public void run() { - DvrUiHelper.requestRecordingCurrentProgram(mMainActivity, - currentChannel, program, true); + DvrUiHelper.requestRecordingCurrentProgram( + mMainActivity, currentChannel, program, true); } }); } } else if (currentChannel != null) { - DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(), + DvrUiHelper.showStopRecordingDialog( + mMainActivity, + currentChannel.getId(), DvrStopRecordingFragment.REASON_USER_STOP, new HalfSizedDialogFragment.OnActionClickListener() { @Override public void onActionClick(long actionId) { if (actionId == DvrStopRecordingFragment.ACTION_STOP) { ScheduledRecording currentRecording = - mDvrManager.getCurrentRecording( - currentChannel.getId()); + mDvrManager.getCurrentRecording(currentChannel.getId()); if (currentRecording != null) { mDvrManager.stopRecording(currentRecording); } @@ -288,8 +324,12 @@ public class PlayControlsRowView extends MenuRowView { } } - private void initializeButton(PlayControlsButton button, int imageResId, - int descriptionId, Integer focusedIconColor, Runnable clickAction) { + private void initializeButton( + PlayControlsButton button, + int imageResId, + int descriptionId, + Integer focusedIconColor, + Runnable clickAction) { button.setImageResId(imageResId); button.setAction(clickAction); if (focusedIconColor != null) { @@ -305,69 +345,77 @@ public class PlayControlsRowView extends MenuRowView { PlayControlsRow playControlsRow = (PlayControlsRow) row; mTvView = playControlsRow.getTvView(); mTimeShiftManager = playControlsRow.getTimeShiftManager(); - mTimeShiftManager.setListener(new TimeShiftManager.Listener() { - @Override - public void onAvailabilityChanged() { - updateMenuVisibility(); - PlayControlsRowView.this.updateAll(false); - } + mTimeShiftManager.setListener( + new TimeShiftManager.Listener() { + @Override + public void onAvailabilityChanged() { + updateMenuVisibility(); + PlayControlsRowView.this.updateAll(false); + } - @Override - public void onPlayStatusChanged(int status) { - updateMenuVisibility(); - if (mTimeShiftManager.isAvailable()) { - updateControls(false); - } - } + @Override + public void onPlayStatusChanged(int status) { + updateMenuVisibility(); + if (mTimeShiftManager.isAvailable()) { + updateControls(false); + } + } - @Override - public void onRecordTimeRangeChanged() { - if (mTimeShiftManager.isAvailable()) { - updateControls(false); - } - } + @Override + public void onRecordTimeRangeChanged() { + if (mTimeShiftManager.isAvailable()) { + updateControls(false); + } + } - @Override - public void onCurrentPositionChanged() { - if (mTimeShiftManager.isAvailable()) { - initializeTimeline(); - updateControls(false); - } - } + @Override + public void onCurrentPositionChanged() { + if (mTimeShiftManager.isAvailable()) { + initializeTimeline(); + updateControls(false); + } + } - @Override - public void onProgramInfoChanged() { - if (mTimeShiftManager.isAvailable()) { - initializeTimeline(); - updateControls(false); - } - } + @Override + public void onProgramInfoChanged() { + if (mTimeShiftManager.isAvailable()) { + initializeTimeline(); + updateControls(false); + } + } - @Override - public void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled) { - // Move focus to the play/pause button when the PREVIOUS, NEXT, REWIND or - // FAST_FORWARD button is clicked and the button becomes disabled. - // No need to update the UI here because the UI will be updated by other callbacks. - if (!enabled && - ((actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS - && mJumpPreviousButton.hasFocus()) - || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND - && mRewindButton.hasFocus()) - || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD - && mFastForwardButton.hasFocus()) - || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT - && mJumpNextButton.hasFocus()))) { - mPlayPauseButton.requestFocus(); - } - } - }); + @Override + public void onActionEnabledChanged( + @TimeShiftActionId int actionId, boolean enabled) { + // Move focus to the play/pause button when the PREVIOUS, NEXT, REWIND or + // FAST_FORWARD button is clicked and the button becomes disabled. + // No need to update the UI here because the UI will be updated by other + // callbacks. + if (!enabled + && ((actionId + == TimeShiftManager + .TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS + && mJumpPreviousButton.hasFocus()) + || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND + && mRewindButton.hasFocus()) + || (actionId + == TimeShiftManager + .TIME_SHIFT_ACTION_ID_FAST_FORWARD + && mFastForwardButton.hasFocus()) + || (actionId + == TimeShiftManager + .TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT + && mJumpNextButton.hasFocus()))) { + mPlayPauseButton.requestFocus(); + } + } + }); // force update to initialize everything updateAll(true); } private void initializeTimeline() { - Program program = mTimeShiftManager.getProgramAt( - mTimeShiftManager.getCurrentPositionMs()); + Program program = mTimeShiftManager.getProgramAt(mTimeShiftManager.getCurrentPositionMs()); mProgramStartTimeMs = program.getStartTimeUtcMillis(); mProgramEndTimeMs = program.getEndTimeUtcMillis(); mProgress.setMax(mProgramEndTimeMs - mProgramStartTimeMs); @@ -441,16 +489,17 @@ public class PlayControlsRowView extends MenuRowView { // Focus may be changed in another message if requestFocus is called in this message. // After the focus is actually changed, hideRippleAnimation should run // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted. - post(new Runnable() { - @Override - public void run() { - mJumpPreviousButton.hideRippleAnimation(); - mRewindButton.hideRippleAnimation(); - mPlayPauseButton.hideRippleAnimation(); - mFastForwardButton.hideRippleAnimation(); - mJumpNextButton.hideRippleAnimation(); - } - }); + post( + new Runnable() { + @Override + public void run() { + mJumpPreviousButton.hideRippleAnimation(); + mRewindButton.hideRippleAnimation(); + mPlayPauseButton.hideRippleAnimation(); + mFastForwardButton.hideRippleAnimation(); + mJumpNextButton.hideRippleAnimation(); + } + }); } @Override @@ -465,9 +514,7 @@ public class PlayControlsRowView extends MenuRowView { } } - /** - * Updates the view contents. It is called from the PlayControlsRow. - */ + /** Updates the view contents. It is called from the PlayControlsRow. */ public void update() { updateAll(false); } @@ -516,13 +563,22 @@ public class PlayControlsRowView extends MenuRowView { private void updateProgress() { if (isEnabled()) { - long progressStartTimeMs = Math.min(mProgramEndTimeMs, - Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); - long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, - Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); - long progressEndTimeMs = Math.min(mProgramEndTimeMs, - Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs())); - mProgress.setProgressRange(progressStartTimeMs - mProgramStartTimeMs, + long progressStartTimeMs = + Math.min( + mProgramEndTimeMs, + Math.max( + mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); + long currentPlayingTimeMs = + Math.min( + mProgramEndTimeMs, + Math.max( + mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); + long progressEndTimeMs = + Math.min( + mProgramEndTimeMs, + Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs())); + mProgress.setProgressRange( + progressStartTimeMs - mProgramStartTimeMs, progressEndTimeMs - mProgramStartTimeMs); mProgress.setProgress(currentPlayingTimeMs - mProgramStartTimeMs); } else { @@ -560,21 +616,24 @@ public class PlayControlsRowView extends MenuRowView { if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PAUSED) { mPlayPauseButton.setImageResId(R.drawable.lb_ic_play); - mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY)); + mPlayPauseButton.setEnabled( + mTimeShiftManager.isActionEnabled(TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY)); } else { mPlayPauseButton.setImageResId(R.drawable.lb_ic_pause); - mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE)); + mPlayPauseButton.setEnabled( + mTimeShiftManager.isActionEnabled(TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE)); } - mJumpPreviousButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)); - mRewindButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)); - mFastForwardButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)); - mJumpNextButton.setEnabled(mTimeShiftManager.isActionEnabled( - TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)); + mJumpPreviousButton.setEnabled( + mTimeShiftManager.isActionEnabled( + TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)); + mRewindButton.setEnabled( + mTimeShiftManager.isActionEnabled(TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)); + mFastForwardButton.setEnabled( + mTimeShiftManager.isActionEnabled( + TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)); + mJumpNextButton.setEnabled( + mTimeShiftManager.isActionEnabled( + TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)); mJumpPreviousButton.setVisibility(VISIBLE); mJumpNextButton.setVisibility(VISIBLE); updateButtonMargin(); @@ -590,8 +649,11 @@ public class PlayControlsRowView extends MenuRowView { if (mTimeShiftManager.getDisplayedPlaySpeed() == TimeShiftManager.PLAY_SPEED_1X) { button.setLabel(null); } else { - button.setLabel(getResources().getString(R.string.play_controls_speed, - mTimeShiftManager.getDisplayedPlaySpeed())); + button.setLabel( + getResources() + .getString( + R.string.play_controls_speed, + mTimeShiftManager.getDisplayedPlaySpeed())); } } @@ -618,12 +680,13 @@ public class PlayControlsRowView extends MenuRowView { } private void updateButtonMargin() { - int numOfVisibleButtons = (mJumpPreviousButton.getVisibility() == View.VISIBLE ? 1 : 0) - + (mRewindButton.getVisibility() == View.VISIBLE ? 1 : 0) - + (mPlayPauseButton.getVisibility() == View.VISIBLE ? 1 : 0) - + (mFastForwardButton.getVisibility() == View.VISIBLE ? 1 : 0) - + (mJumpNextButton.getVisibility() == View.VISIBLE ? 1 : 0) - + (mRecordButton.getVisibility() == View.VISIBLE ? 1 : 0); + int numOfVisibleButtons = + (mJumpPreviousButton.getVisibility() == View.VISIBLE ? 1 : 0) + + (mRewindButton.getVisibility() == View.VISIBLE ? 1 : 0) + + (mPlayPauseButton.getVisibility() == View.VISIBLE ? 1 : 0) + + (mFastForwardButton.getVisibility() == View.VISIBLE ? 1 : 0) + + (mJumpNextButton.getVisibility() == View.VISIBLE ? 1 : 0) + + (mRecordButton.getVisibility() == View.VISIBLE ? 1 : 0); boolean useCompactLayout = numOfVisibleButtons > NORMAL_WIDTH_MAX_BUTTON_COUNT; if (mUseCompactLayout == useCompactLayout) { return; diff --git a/src/com/android/tv/menu/PlaybackProgressBar.java b/src/com/android/tv/menu/PlaybackProgressBar.java index e8061bc6..398afe13 100644 --- a/src/com/android/tv/menu/PlaybackProgressBar.java +++ b/src/com/android/tv/menu/PlaybackProgressBar.java @@ -24,12 +24,9 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.view.View; - import com.android.tv.R; -/** - * A progress bar control which has two progresses which start in the middle of the control. - */ +/** A progress bar control which has two progresses which start in the middle of the control. */ public class PlaybackProgressBar extends View { private final LayerDrawable mProgressDrawable; private final Drawable mPrimaryDrawable; @@ -51,11 +48,12 @@ public class PlaybackProgressBar extends View { this(context, attrs, defStyleAttr, 0); } - public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + public PlaybackProgressBar( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes); + TypedArray a = + context.obtainStyledAttributes( + attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes); mProgressDrawable = (LayerDrawable) a.getDrawable(R.styleable.PlaybackProgressBar_progressDrawable); mPrimaryDrawable = mProgressDrawable.findDrawableByLayerId(android.R.id.progress); @@ -98,9 +96,7 @@ public class PlaybackProgressBar extends View { } } - /** - * Sets the start and end position of the progress. - */ + /** Sets the start and end position of the progress. */ public void setProgressRange(long start, long end) { start = constrain(start, 0, mMax); end = constrain(end, start, mMax); @@ -112,9 +108,7 @@ public class PlaybackProgressBar extends View { } } - /** - * Sets the progress position. - */ + /** Sets the progress position. */ public void setProgress(long progress) { progress = constrain(progress, mProgressStart, mProgressEnd); if (progress != mProgress) { diff --git a/src/com/android/tv/menu/SimpleCardView.java b/src/com/android/tv/menu/SimpleCardView.java index fc5192da..e8ecdc72 100644 --- a/src/com/android/tv/menu/SimpleCardView.java +++ b/src/com/android/tv/menu/SimpleCardView.java @@ -19,9 +19,7 @@ package com.android.tv.menu; import android.content.Context; import android.util.AttributeSet; -/** - * A view to render a guide card. - */ +/** A view to render a guide card. */ public class SimpleCardView extends BaseCardView<ChannelsRowItem> { public SimpleCardView(Context context) { diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index 6e035f22..55affb59 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -19,18 +19,16 @@ package com.android.tv.menu; import android.content.Context; import android.media.tv.TvTrackInfo; import android.support.annotation.VisibleForTesting; - -import com.android.tv.Features; +import com.android.tv.TvFeatures; import com.android.tv.TvOptionsManager; -import com.android.tv.customization.CustomAction; +import com.android.tv.common.customization.CustomAction; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.DisplayMode; import com.android.tv.ui.TvViewUiManager; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; import com.android.tv.ui.sidepanel.DeveloperOptionFragment; import com.android.tv.ui.sidepanel.DisplayModeFragment; import com.android.tv.ui.sidepanel.MultiAudioFragment; -import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.List; @@ -47,12 +45,12 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { List<MenuAction> actionList = new ArrayList<>(); actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION); actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION); - if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { + if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) { actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION); } actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); actionList.add(MenuAction.MORE_CHANNELS_ACTION); - if (Utils.isDeveloper()) { + if (CommonUtils.isDeveloper()) { actionList.add(MenuAction.DEV_ACTION); } actionList.add(MenuAction.SETTINGS_ACTION); @@ -87,7 +85,8 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { private boolean updatePipAction() { if (containsItem(MenuAction.SYSTEMWIDE_PIP_ACTION)) { - return MenuAction.setEnabled(MenuAction.SYSTEMWIDE_PIP_ACTION, + return MenuAction.setEnabled( + MenuAction.SYSTEMWIDE_PIP_ACTION, !getMainActivity().isScreenBlockedByResourceConflictOrParentalControl()); } return false; @@ -103,41 +102,50 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { private boolean updateDisplayModeAction() { TvViewUiManager uiManager = getMainActivity().getTvViewUiManager(); - boolean enabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) - || uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM); + boolean enabled = + uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL) + || uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM); // Use "|" operator for non-short-circuit evaluation. return MenuAction.setEnabled(MenuAction.SELECT_DISPLAY_MODE_ACTION, enabled) | updateActionDescription(MenuAction.SELECT_DISPLAY_MODE_ACTION); } private boolean updateActionDescription(MenuAction action) { - return MenuAction.setActionDescription(action, - getMainActivity().getTvOptionsManager().getOptionString(action.getType())); + return MenuAction.setActionDescription( + action, getMainActivity().getTvOptionsManager().getOptionString(action.getType())); } @Override protected void executeBaseAction(int type) { switch (type) { case TvOptionsManager.OPTION_CLOSED_CAPTIONS: - getMainActivity().getOverlayManager().getSideFragmentManager() + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() .show(new ClosedCaptionFragment()); break; case TvOptionsManager.OPTION_DISPLAY_MODE: - getMainActivity().getOverlayManager().getSideFragmentManager() + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() .show(new DisplayModeFragment()); break; case TvOptionsManager.OPTION_SYSTEMWIDE_PIP: getMainActivity().enterPictureInPictureMode(); break; case TvOptionsManager.OPTION_MULTI_AUDIO: - getMainActivity().getOverlayManager().getSideFragmentManager() + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() .show(new MultiAudioFragment()); break; case TvOptionsManager.OPTION_MORE_CHANNELS: getMainActivity().showMerchantCollection(); break; case TvOptionsManager.OPTION_DEVELOPER: - getMainActivity().getOverlayManager().getSideFragmentManager() + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() .show(new DeveloperOptionFragment()); break; case TvOptionsManager.OPTION_SETTINGS: @@ -145,4 +153,4 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { break; } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/onboarding/NewSourcesFragment.java b/src/com/android/tv/onboarding/NewSourcesFragment.java index 8509b50c..f3b077a7 100644 --- a/src/com/android/tv/onboarding/NewSourcesFragment.java +++ b/src/com/android/tv/onboarding/NewSourcesFragment.java @@ -23,28 +23,17 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.ui.setup.SetupActionHelper; -import com.android.tv.util.SetupUtils; -/** - * A fragment for new channel source info/setup. - */ +/** A fragment for new channel source info/setup. */ public class NewSourcesFragment extends Fragment { - /** - * The action category. - */ - public static final String ACTION_CATEOGRY = - "com.android.tv.onboarding.NewSourcesFragment"; - /** - * An action to show the setup screen. - */ + /** The action category. */ + public static final String ACTION_CATEOGRY = "com.android.tv.onboarding.NewSourcesFragment"; + /** An action to show the setup screen. */ public static final int ACTION_SETUP = 1; - /** - * An action to close this fragment. - */ + /** An action to close this fragment. */ public static final int ACTION_SKIP = 2; public NewSourcesFragment() { @@ -57,19 +46,20 @@ public class NewSourcesFragment extends Fragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_new_sources, container, false); initializeButton(view.findViewById(R.id.setup), ACTION_SETUP); initializeButton(view.findViewById(R.id.skip), ACTION_SKIP); - SetupUtils.getInstance(getActivity()).markAllInputsRecognized(TvApplication - .getSingletons(getActivity()).getTvInputManagerHelper()); + TvSingletons singletons = TvSingletons.getSingletons(getActivity()); + singletons.getSetupUtils().markAllInputsRecognized(singletons.getTvInputManagerHelper()); view.requestFocus(); return view; } private void initializeButton(View view, int actionId) { - view.setOnClickListener(SetupActionHelper.createOnClickListenerForAction(this, - ACTION_CATEOGRY, actionId, null)); + view.setOnClickListener( + SetupActionHelper.createOnClickListenerForAction( + this, ACTION_CATEOGRY, actionId, null)); } } diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index 45205c4c..a1cf9de1 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -26,17 +26,15 @@ import android.media.tv.TvInputInfo; import android.os.Bundle; import android.support.annotation.NonNull; import android.widget.Toast; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.SetupPassthroughActivity; -import com.android.tv.TvApplication; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.TvSingletons; import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.data.ChannelDataManager; import com.android.tv.util.OnboardingUtils; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; @@ -51,29 +49,31 @@ public class OnboardingActivity extends SetupActivity { private ChannelDataManager mChannelDataManager; private TvInputManagerHelper mInputManager; - private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - SetupUtils.getInstance(OnboardingActivity.this).markNewChannelsBrowsable(); - } + private SetupUtils mSetupUtils; + private final ChannelDataManager.Listener mChannelListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + mSetupUtils.markNewChannelsBrowsable(); + } - @Override - public void onChannelListUpdated() { } + @Override + public void onChannelListUpdated() {} - @Override - public void onChannelBrowsableChanged() { } - }; + @Override + public void onChannelBrowsableChanged() {} + }; /** * Returns an intent to start {@link OnboardingActivity}. * * @param context context to create an intent. Should not be {@code null}. * @param intentAfterCompletion intent which will be used to start a new activity when this - * activity finishes. Should not be {@code null}. + * activity finishes. Should not be {@code null}. */ - public static Intent buildIntent(@NonNull Context context, - @NonNull Intent intentAfterCompletion) { + public static Intent buildIntent( + @NonNull Context context, @NonNull Intent intentAfterCompletion) { return new Intent(context, OnboardingActivity.class) .putExtra(OnboardingActivity.KEY_INTENT_AFTER_COMPLETION, intentAfterCompletion); } @@ -81,19 +81,21 @@ public class OnboardingActivity extends SetupActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ApplicationSingletons singletons = TvApplication.getSingletons(this); + TvSingletons singletons = TvSingletons.getSingletons(this); mInputManager = singletons.getTvInputManagerHelper(); + mSetupUtils = singletons.getSetupUtils(); if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { mChannelDataManager = singletons.getChannelDataManager(); // Make the channels of the new inputs which have been setup outside Live TV // browsable. if (mChannelDataManager.isDbLoadFinished()) { - SetupUtils.getInstance(this).markNewChannelsBrowsable(); + mSetupUtils.markNewChannelsBrowsable(); } else { mChannelDataManager.addListener(mChannelListener); } } else { - requestPermissions(new String[] {PermissionUtils.PERMISSION_READ_TV_LISTINGS}, + requestPermissions( + new String[] {PermissionUtils.PERMISSION_READ_TV_LISTINGS}, PERMISSIONS_REQUEST_READ_TV_LISTINGS); } } @@ -109,32 +111,35 @@ public class OnboardingActivity extends SetupActivity { @Override protected Fragment onCreateInitialFragment() { if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { - return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment() + return OnboardingUtils.isFirstRunWithCurrentVersion(this) + ? new WelcomeFragment() : new SetupSourcesFragment(); } return null; } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { finish(); - Intent intentForNextActivity = getIntent().getParcelableExtra( - KEY_INTENT_AFTER_COMPLETION); + Intent intentForNextActivity = + getIntent().getParcelableExtra(KEY_INTENT_AFTER_COMPLETION); startActivity(buildIntent(this, intentForNextActivity)); } else { - Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied, - Toast.LENGTH_LONG).show(); + Toast.makeText( + this, + R.string.msg_read_tv_listing_permission_denied, + Toast.LENGTH_LONG) + .show(); finish(); } } } private void finishActivity() { - Intent intentForNextActivity = getIntent().getParcelableExtra( - KEY_INTENT_AFTER_COMPLETION); + Intent intentForNextActivity = getIntent().getParcelableExtra(KEY_INTENT_AFTER_COMPLETION); if (intentForNextActivity != null) { startActivity(intentForNextActivity); } @@ -142,12 +147,14 @@ public class OnboardingActivity extends SetupActivity { } private void showMerchantCollection() { - executeActionWithDelay(new Runnable() { - @Override - public void run() { - startActivity(OnboardingUtils.ONLINE_STORE_INTENT); - } - }, SHOW_RIPPLE_DURATION_MS); + executeActionWithDelay( + new Runnable() { + @Override + public void run() { + startActivity(OnboardingUtils.ONLINE_STORE_INTENT); + } + }, + SHOW_RIPPLE_DURATION_MS); } @Override @@ -167,42 +174,55 @@ public class OnboardingActivity extends SetupActivity { case SetupSourcesFragment.ACTION_ONLINE_STORE: showMerchantCollection(); return true; - case SetupSourcesFragment.ACTION_SETUP_INPUT: { - String inputId = params.getString( - SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = TvCommonUtils.createSetupIntent(input); - if (intent == null) { - Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) - .show(); + case SetupSourcesFragment.ACTION_SETUP_INPUT: + { + String inputId = + params.getString( + SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + Intent intent = CommonUtils.createSetupIntent(input); + if (intent == null) { + Toast.makeText( + this, + R.string.msg_no_setup_activity, + Toast.LENGTH_SHORT) + .show(); + return true; + } + // Even though other app can handle the intent, the setup launched by + // Live + // channels should go through Live channels SetupPassthroughActivity. + intent.setComponent( + new ComponentName(this, SetupPassthroughActivity.class)); + try { + // Now we know that the user intends to set up this input. Grant + // permission for writing EPG data. + SetupUtils.grantEpgPermission( + this, input.getServiceInfo().packageName); + startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); + } catch (ActivityNotFoundException e) { + Toast.makeText( + this, + getString( + R.string.msg_unable_to_start_setup_activity, + input.loadLabel(this)), + Toast.LENGTH_SHORT) + .show(); + } return true; } - // Even though other app can handle the intent, the setup launched by Live - // channels should go through Live channels SetupPassthroughActivity. - intent.setComponent(new ComponentName(this, - SetupPassthroughActivity.class)); - try { - // Now we know that the user intends to set up this input. Grant - // permission for writing EPG data. - SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName); - startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, - getString(R.string.msg_unable_to_start_setup_activity, - input.loadLabel(this)), Toast.LENGTH_SHORT).show(); - } - return true; - } - case SetupMultiPaneFragment.ACTION_DONE: { - ChannelDataManager manager = TvApplication.getSingletons( - OnboardingActivity.this).getChannelDataManager(); - if (manager.getChannelCount() == 0) { - finish(); - } else { - finishActivity(); + case SetupMultiPaneFragment.ACTION_DONE: + { + ChannelDataManager manager = + TvSingletons.getSingletons(OnboardingActivity.this) + .getChannelDataManager(); + if (manager.getChannelCount() == 0) { + finish(); + } else { + finishActivity(); + } + return true; } - return true; - } } break; } diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java index f56daec5..f032f622 100644 --- a/src/com/android/tv/onboarding/SetupSourcesFragment.java +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -30,41 +30,30 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.ui.setup.SetupGuidedStepFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.TvInputNewComparator; -import com.android.tv.tuner.TunerInputController; import com.android.tv.ui.GuidedActionsStylistWithDivider; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; - import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * A fragment for channel source info/setup. - */ +/** A fragment for channel source info/setup. */ public class SetupSourcesFragment extends SetupMultiPaneFragment { - /** - * The action category for the actions which is fired from this fragment. - */ - public static final String ACTION_CATEGORY = - "com.android.tv.onboarding.SetupSourcesFragment"; - /** - * An action to open the merchant collection. - */ + /** The action category for the actions which is fired from this fragment. */ + public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment"; + /** An action to open the merchant collection. */ public static final int ACTION_ONLINE_STORE = 1; /** * An action to show the setup activity of TV input. - * <p> - * This action is not added to the action list. This is sent outside of the fragment. - * Use {@link #ACTION_PARAM_KEY_INPUT_ID} to get the input ID from the parameter. + * + * <p>This action is not added to the action list. This is sent outside of the fragment. Use + * {@link #ACTION_PARAM_KEY_INPUT_ID} to get the input ID from the parameter. */ public static final int ACTION_SETUP_INPUT = 2; @@ -77,10 +66,10 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { private static final String SETUP_TRACKER_LABEL = "Setup fragment"; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); + TvSingletons.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); return view; } @@ -130,82 +119,87 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { private int mPendingAction = PENDING_ACTION_NONE; - private final TvInputCallback mInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - handleInputChanged(); - } + private final TvInputCallback mInputCallback = + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + handleInputChanged(); + } - @Override - public void onInputRemoved(String inputId) { - handleInputChanged(); - } + @Override + public void onInputRemoved(String inputId) { + handleInputChanged(); + } - @Override - public void onInputUpdated(String inputId) { - handleInputChanged(); - } + @Override + public void onInputUpdated(String inputId) { + handleInputChanged(); + } - @Override - public void onTvInputInfoUpdated(TvInputInfo inputInfo) { - handleInputChanged(); - } + @Override + public void onTvInputInfoUpdated(TvInputInfo inputInfo) { + handleInputChanged(); + } - private void handleInputChanged() { - // The actions created while enter transition is running will not be included in the - // fragment transition. - if (mParentFragment.isEnterTransitionRunning()) { - mPendingAction = PENDING_ACTION_INPUT_CHANGED; - return; - } - buildInputs(); - updateActions(); - } - }; + private void handleInputChanged() { + // The actions created while enter transition is running will not be + // included in the + // fragment transition. + if (mParentFragment.isEnterTransitionRunning()) { + mPendingAction = PENDING_ACTION_INPUT_CHANGED; + return; + } + buildInputs(); + updateActions(); + } + }; - private final ChannelDataManager.Listener mChannelDataManagerListener - = new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - handleChannelChanged(); - } + private final ChannelDataManager.Listener mChannelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + handleChannelChanged(); + } - @Override - public void onChannelListUpdated() { - handleChannelChanged(); - } + @Override + public void onChannelListUpdated() { + handleChannelChanged(); + } - @Override - public void onChannelBrowsableChanged() { - handleChannelChanged(); - } + @Override + public void onChannelBrowsableChanged() { + handleChannelChanged(); + } - private void handleChannelChanged() { - // The actions created while enter transition is running will not be included in the - // fragment transition. - if (mParentFragment.isEnterTransitionRunning()) { - if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { - mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; + private void handleChannelChanged() { + // The actions created while enter transition is running will not be + // included in the + // fragment transition. + if (mParentFragment.isEnterTransitionRunning()) { + if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { + mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; + } + return; + } + updateActions(); } - return; - } - updateActions(); - } - }; + }; @Override public void onCreate(Bundle savedInstanceState) { Context context = getActivity(); - ApplicationSingletons app = TvApplication.getSingletons(context); - mInputManager = app.getTvInputManagerHelper(); - mChannelDataManager = app.getChannelDataManager(); - mSetupUtils = SetupUtils.getInstance(context); + TvSingletons singletons = TvSingletons.getSingletons(context); + mInputManager = singletons.getTvInputManagerHelper(); + mChannelDataManager = singletons.getChannelDataManager(); + mSetupUtils = singletons.getSetupUtils(); buildInputs(); mInputManager.addCallback(mInputCallback); mChannelDataManager.addListener(mChannelDataManagerListener); super.onCreate(savedInstanceState); mParentFragment = (SetupSourcesFragment) getParentFragment(); - TunerInputController.executeNetworkTunerDiscoveryAsyncTask(getContext()); + singletons + .getTunerInputController() + .executeNetworkTunerDiscoveryAsyncTask(getContext()); } @Override @@ -229,8 +223,8 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { } @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { + public void onCreateActions( + @NonNull List<GuidedAction> actions, Bundle savedInstanceState) { createActionsInternal(actions); } @@ -274,24 +268,26 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { int position = 0; if (mDoneInputStartIndex > 0) { // Need a "New" category - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_HEADER) - .title(null) - .description(getString(R.string.setup_category_new)) - .focusable(false) - .infoOnly(true) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_HEADER) + .title(null) + .description(getString(R.string.setup_category_new)) + .focusable(false) + .infoOnly(true) + .build()); } for (int i = 0; i < mInputs.size(); ++i) { if (i == mDoneInputStartIndex) { ++position; - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_HEADER) - .title(null) - .description(getString(R.string.setup_category_done)) - .focusable(false) - .infoOnly(true) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_HEADER) + .title(null) + .description(getString(R.string.setup_category_done)) + .focusable(false) + .infoOnly(true) + .build()); } TvInputInfo input = mInputs.get(i); String inputId = input.getId(); @@ -301,8 +297,12 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { if (channelCount == 0) { description = getString(R.string.setup_input_no_channels); } else { - description = getResources().getQuantityString( - R.plurals.setup_input_channels, channelCount, channelCount); + description = + getResources() + .getQuantityString( + R.plurals.setup_input_channels, + channelCount, + channelCount); } } else if (i >= mKnownInputStartIndex) { description = getString(R.string.setup_input_setup_now); @@ -313,11 +313,12 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { if (input.getId().equals(mNewlyAddedInputId)) { newPosition = position; } - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_INPUT_START + i) - .title(input.loadLabel(getActivity()).toString()) - .description(description) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_INPUT_START + i) + .title(input.loadLabel(getActivity()).toString()) + .description(description) + .build()); } if (mInputs.size() > 0) { // Divider @@ -326,12 +327,13 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { } // online store action ++position; - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ONLINE_STORE) - .title(getString(R.string.setup_store_action_title)) - .description(getString(R.string.setup_store_action_description)) - .icon(R.drawable.ic_store) - .build()); + actions.add( + new GuidedAction.Builder(getActivity()) + .id(ACTION_ONLINE_STORE) + .title(getString(R.string.setup_store_action_title)) + .description(getString(R.string.setup_store_action_description)) + .icon(R.drawable.ic_store) + .build()); if (newPosition != -1) { VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); @@ -367,6 +369,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { case PENDING_ACTION_CHANNEL_CHANGED: updateActions(); break; + default: // fall out } mPendingAction = PENDING_ACTION_NONE; } @@ -382,17 +385,19 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment { if (descriptionView != null) { if (action.getId() == ACTION_HEADER) { descriptionView.setAlpha(ALPHA_CATEGORY); - descriptionView.setTextColor(getResources().getColor(R.color.setup_category, - null)); - descriptionView.setTypeface(Typeface.create( - getString(R.string.condensed_font), 0)); + descriptionView.setTextColor( + getResources().getColor(R.color.setup_category, null)); + descriptionView.setTypeface( + Typeface.create(getString(R.string.condensed_font), 0)); } else { descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION); - descriptionView.setTextColor(getResources().getColor( - R.color.common_setup_input_description, null)); + descriptionView.setTextColor( + getResources() + .getColor(R.color.common_setup_input_description, null)); descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0)); } } + setAccessibilityDelegate(vh, action); } } } diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java index f12233e9..8c119a8a 100644 --- a/src/com/android/tv/onboarding/WelcomeFragment.java +++ b/src/com/android/tv/onboarding/WelcomeFragment.java @@ -16,33 +16,37 @@ package com.android.tv.onboarding; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS; + import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v17.leanback.app.OnboardingFragment; +import android.text.Editable; +import android.text.TextWatcher; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; import android.widget.ImageView; - +import android.widget.TextView; import com.android.tv.R; import com.android.tv.common.ui.setup.SetupActionHelper; import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; - import java.util.ArrayList; import java.util.List; -/** - * A fragment for the onboarding welcome screen. - */ +/** A fragment for the onboarding welcome screen. */ public class WelcomeFragment extends OnboardingFragment { - public static final String ACTION_CATEGORY = "comgoogle.android.tv.onboarding.WelcomeFragment"; + public static final String ACTION_CATEGORY = "com.android.tv.onboarding.WelcomeFragment"; public static final int ACTION_NEXT = 1; private static final long START_DELAY_CLOUD_MS = 33; @@ -56,523 +60,523 @@ public class WelcomeFragment extends OnboardingFragment { // TODO: Use animator list xml. private static final int[] TV_FRAMES_1_START = { - R.drawable.tv_1a_01, - R.drawable.tv_1a_02, - R.drawable.tv_1a_03, - R.drawable.tv_1a_04, - R.drawable.tv_1a_05, - R.drawable.tv_1a_06, - R.drawable.tv_1a_07, - R.drawable.tv_1a_08, - R.drawable.tv_1a_09, - R.drawable.tv_1a_10, - R.drawable.tv_1a_11, - R.drawable.tv_1a_12, - R.drawable.tv_1a_13, - R.drawable.tv_1a_14, - R.drawable.tv_1a_15, - R.drawable.tv_1a_16, - R.drawable.tv_1a_17, - R.drawable.tv_1a_18, - R.drawable.tv_1a_19, - R.drawable.tv_1a_20 + R.drawable.tv_1a_01, + R.drawable.tv_1a_02, + R.drawable.tv_1a_03, + R.drawable.tv_1a_04, + R.drawable.tv_1a_05, + R.drawable.tv_1a_06, + R.drawable.tv_1a_07, + R.drawable.tv_1a_08, + R.drawable.tv_1a_09, + R.drawable.tv_1a_10, + R.drawable.tv_1a_11, + R.drawable.tv_1a_12, + R.drawable.tv_1a_13, + R.drawable.tv_1a_14, + R.drawable.tv_1a_15, + R.drawable.tv_1a_16, + R.drawable.tv_1a_17, + R.drawable.tv_1a_18, + R.drawable.tv_1a_19, + R.drawable.tv_1a_20 }; private static final int[] TV_FRAMES_1_END = { - R.drawable.tv_1b_01, - R.drawable.tv_1b_02, - R.drawable.tv_1b_03, - R.drawable.tv_1b_04, - R.drawable.tv_1b_05, - R.drawable.tv_1b_06, - R.drawable.tv_1b_07, - R.drawable.tv_1b_08, - R.drawable.tv_1b_09, - R.drawable.tv_1b_10, - R.drawable.tv_1b_11 + R.drawable.tv_1b_01, + R.drawable.tv_1b_02, + R.drawable.tv_1b_03, + R.drawable.tv_1b_04, + R.drawable.tv_1b_05, + R.drawable.tv_1b_06, + R.drawable.tv_1b_07, + R.drawable.tv_1b_08, + R.drawable.tv_1b_09, + R.drawable.tv_1b_10, + R.drawable.tv_1b_11 }; private static final int[] TV_FRAMES_2_START = { - R.drawable.tv_5a_0, - R.drawable.tv_5a_1, - R.drawable.tv_5a_2, - R.drawable.tv_5a_3, - R.drawable.tv_5a_4, - R.drawable.tv_5a_5, - R.drawable.tv_5a_6, - R.drawable.tv_5a_7, - R.drawable.tv_5a_8, - R.drawable.tv_5a_9, - R.drawable.tv_5a_10, - R.drawable.tv_5a_11, - R.drawable.tv_5a_12, - R.drawable.tv_5a_13, - R.drawable.tv_5a_14, - R.drawable.tv_5a_15, - R.drawable.tv_5a_16, - R.drawable.tv_5a_17, - R.drawable.tv_5a_18, - R.drawable.tv_5a_19, - R.drawable.tv_5a_20, - R.drawable.tv_5a_21, - R.drawable.tv_5a_22, - R.drawable.tv_5a_23, - R.drawable.tv_5a_24, - R.drawable.tv_5a_25, - R.drawable.tv_5a_26, - R.drawable.tv_5a_27, - R.drawable.tv_5a_28, - R.drawable.tv_5a_29, - R.drawable.tv_5a_30, - R.drawable.tv_5a_31, - R.drawable.tv_5a_32, - R.drawable.tv_5a_33, - R.drawable.tv_5a_34, - R.drawable.tv_5a_35, - R.drawable.tv_5a_36, - R.drawable.tv_5a_37, - R.drawable.tv_5a_38, - R.drawable.tv_5a_39, - R.drawable.tv_5a_40, - R.drawable.tv_5a_41, - R.drawable.tv_5a_42, - R.drawable.tv_5a_43, - R.drawable.tv_5a_44, - R.drawable.tv_5a_45, - R.drawable.tv_5a_46, - R.drawable.tv_5a_47, - R.drawable.tv_5a_48, - R.drawable.tv_5a_49, - R.drawable.tv_5a_50, - R.drawable.tv_5a_51, - R.drawable.tv_5a_52, - R.drawable.tv_5a_53, - R.drawable.tv_5a_54, - R.drawable.tv_5a_55, - R.drawable.tv_5a_56, - R.drawable.tv_5a_57, - R.drawable.tv_5a_58, - R.drawable.tv_5a_59, - R.drawable.tv_5a_60, - R.drawable.tv_5a_61, - R.drawable.tv_5a_62, - R.drawable.tv_5a_63, - R.drawable.tv_5a_64, - R.drawable.tv_5a_65, - R.drawable.tv_5a_66, - R.drawable.tv_5a_67, - R.drawable.tv_5a_68, - R.drawable.tv_5a_69, - R.drawable.tv_5a_70, - R.drawable.tv_5a_71, - R.drawable.tv_5a_72, - R.drawable.tv_5a_73, - R.drawable.tv_5a_74, - R.drawable.tv_5a_75, - R.drawable.tv_5a_76, - R.drawable.tv_5a_77, - R.drawable.tv_5a_78, - R.drawable.tv_5a_79, - R.drawable.tv_5a_80, - R.drawable.tv_5a_81, - R.drawable.tv_5a_82, - R.drawable.tv_5a_83, - R.drawable.tv_5a_84, - R.drawable.tv_5a_85, - R.drawable.tv_5a_86, - R.drawable.tv_5a_87, - R.drawable.tv_5a_88, - R.drawable.tv_5a_89, - R.drawable.tv_5a_90, - R.drawable.tv_5a_91, - R.drawable.tv_5a_92, - R.drawable.tv_5a_93, - R.drawable.tv_5a_94, - R.drawable.tv_5a_95, - R.drawable.tv_5a_96, - R.drawable.tv_5a_97, - R.drawable.tv_5a_98, - R.drawable.tv_5a_99, - R.drawable.tv_5a_100, - R.drawable.tv_5a_101, - R.drawable.tv_5a_102, - R.drawable.tv_5a_103, - R.drawable.tv_5a_104, - R.drawable.tv_5a_105, - R.drawable.tv_5a_106, - R.drawable.tv_5a_107, - R.drawable.tv_5a_108, - R.drawable.tv_5a_109, - R.drawable.tv_5a_110, - R.drawable.tv_5a_111, - R.drawable.tv_5a_112, - R.drawable.tv_5a_113, - R.drawable.tv_5a_114, - R.drawable.tv_5a_115, - R.drawable.tv_5a_116, - R.drawable.tv_5a_117, - R.drawable.tv_5a_118, - R.drawable.tv_5a_119, - R.drawable.tv_5a_120, - R.drawable.tv_5a_121, - R.drawable.tv_5a_122, - R.drawable.tv_5a_123, - R.drawable.tv_5a_124, - R.drawable.tv_5a_125, - R.drawable.tv_5a_126, - R.drawable.tv_5a_127, - R.drawable.tv_5a_128, - R.drawable.tv_5a_129, - R.drawable.tv_5a_130, - R.drawable.tv_5a_131, - R.drawable.tv_5a_132, - R.drawable.tv_5a_133, - R.drawable.tv_5a_134, - R.drawable.tv_5a_135, - R.drawable.tv_5a_136, - R.drawable.tv_5a_137, - R.drawable.tv_5a_138, - R.drawable.tv_5a_139, - R.drawable.tv_5a_140, - R.drawable.tv_5a_141, - R.drawable.tv_5a_142, - R.drawable.tv_5a_143, - R.drawable.tv_5a_144, - R.drawable.tv_5a_145, - R.drawable.tv_5a_146, - R.drawable.tv_5a_147, - R.drawable.tv_5a_148, - R.drawable.tv_5a_149, - R.drawable.tv_5a_150, - R.drawable.tv_5a_151, - R.drawable.tv_5a_152, - R.drawable.tv_5a_153, - R.drawable.tv_5a_154, - R.drawable.tv_5a_155, - R.drawable.tv_5a_156, - R.drawable.tv_5a_157, - R.drawable.tv_5a_158, - R.drawable.tv_5a_159, - R.drawable.tv_5a_160, - R.drawable.tv_5a_161, - R.drawable.tv_5a_162, - R.drawable.tv_5a_163, - R.drawable.tv_5a_164, - R.drawable.tv_5a_165, - R.drawable.tv_5a_166, - R.drawable.tv_5a_167, - R.drawable.tv_5a_168, - R.drawable.tv_5a_169, - R.drawable.tv_5a_170, - R.drawable.tv_5a_171, - R.drawable.tv_5a_172, - R.drawable.tv_5a_173, - R.drawable.tv_5a_174, - R.drawable.tv_5a_175, - R.drawable.tv_5a_176, - R.drawable.tv_5a_177, - R.drawable.tv_5a_178, - R.drawable.tv_5a_179, - R.drawable.tv_5a_180, - R.drawable.tv_5a_181, - R.drawable.tv_5a_182, - R.drawable.tv_5a_183, - R.drawable.tv_5a_184, - R.drawable.tv_5a_185, - R.drawable.tv_5a_186, - R.drawable.tv_5a_187, - R.drawable.tv_5a_188, - R.drawable.tv_5a_189, - R.drawable.tv_5a_190, - R.drawable.tv_5a_191, - R.drawable.tv_5a_192, - R.drawable.tv_5a_193, - R.drawable.tv_5a_194, - R.drawable.tv_5a_195, - R.drawable.tv_5a_196, - R.drawable.tv_5a_197, - R.drawable.tv_5a_198, - R.drawable.tv_5a_199, - R.drawable.tv_5a_200, - R.drawable.tv_5a_201, - R.drawable.tv_5a_202, - R.drawable.tv_5a_203, - R.drawable.tv_5a_204, - R.drawable.tv_5a_205, - R.drawable.tv_5a_206, - R.drawable.tv_5a_207, - R.drawable.tv_5a_208, - R.drawable.tv_5a_209, - R.drawable.tv_5a_210, - R.drawable.tv_5a_211, - R.drawable.tv_5a_212, - R.drawable.tv_5a_213, - R.drawable.tv_5a_214, - R.drawable.tv_5a_215, - R.drawable.tv_5a_216, - R.drawable.tv_5a_217, - R.drawable.tv_5a_218, - R.drawable.tv_5a_219, - R.drawable.tv_5a_220, - R.drawable.tv_5a_221, - R.drawable.tv_5a_222, - R.drawable.tv_5a_223, - R.drawable.tv_5a_224 + R.drawable.tv_5a_0, + R.drawable.tv_5a_1, + R.drawable.tv_5a_2, + R.drawable.tv_5a_3, + R.drawable.tv_5a_4, + R.drawable.tv_5a_5, + R.drawable.tv_5a_6, + R.drawable.tv_5a_7, + R.drawable.tv_5a_8, + R.drawable.tv_5a_9, + R.drawable.tv_5a_10, + R.drawable.tv_5a_11, + R.drawable.tv_5a_12, + R.drawable.tv_5a_13, + R.drawable.tv_5a_14, + R.drawable.tv_5a_15, + R.drawable.tv_5a_16, + R.drawable.tv_5a_17, + R.drawable.tv_5a_18, + R.drawable.tv_5a_19, + R.drawable.tv_5a_20, + R.drawable.tv_5a_21, + R.drawable.tv_5a_22, + R.drawable.tv_5a_23, + R.drawable.tv_5a_24, + R.drawable.tv_5a_25, + R.drawable.tv_5a_26, + R.drawable.tv_5a_27, + R.drawable.tv_5a_28, + R.drawable.tv_5a_29, + R.drawable.tv_5a_30, + R.drawable.tv_5a_31, + R.drawable.tv_5a_32, + R.drawable.tv_5a_33, + R.drawable.tv_5a_34, + R.drawable.tv_5a_35, + R.drawable.tv_5a_36, + R.drawable.tv_5a_37, + R.drawable.tv_5a_38, + R.drawable.tv_5a_39, + R.drawable.tv_5a_40, + R.drawable.tv_5a_41, + R.drawable.tv_5a_42, + R.drawable.tv_5a_43, + R.drawable.tv_5a_44, + R.drawable.tv_5a_45, + R.drawable.tv_5a_46, + R.drawable.tv_5a_47, + R.drawable.tv_5a_48, + R.drawable.tv_5a_49, + R.drawable.tv_5a_50, + R.drawable.tv_5a_51, + R.drawable.tv_5a_52, + R.drawable.tv_5a_53, + R.drawable.tv_5a_54, + R.drawable.tv_5a_55, + R.drawable.tv_5a_56, + R.drawable.tv_5a_57, + R.drawable.tv_5a_58, + R.drawable.tv_5a_59, + R.drawable.tv_5a_60, + R.drawable.tv_5a_61, + R.drawable.tv_5a_62, + R.drawable.tv_5a_63, + R.drawable.tv_5a_64, + R.drawable.tv_5a_65, + R.drawable.tv_5a_66, + R.drawable.tv_5a_67, + R.drawable.tv_5a_68, + R.drawable.tv_5a_69, + R.drawable.tv_5a_70, + R.drawable.tv_5a_71, + R.drawable.tv_5a_72, + R.drawable.tv_5a_73, + R.drawable.tv_5a_74, + R.drawable.tv_5a_75, + R.drawable.tv_5a_76, + R.drawable.tv_5a_77, + R.drawable.tv_5a_78, + R.drawable.tv_5a_79, + R.drawable.tv_5a_80, + R.drawable.tv_5a_81, + R.drawable.tv_5a_82, + R.drawable.tv_5a_83, + R.drawable.tv_5a_84, + R.drawable.tv_5a_85, + R.drawable.tv_5a_86, + R.drawable.tv_5a_87, + R.drawable.tv_5a_88, + R.drawable.tv_5a_89, + R.drawable.tv_5a_90, + R.drawable.tv_5a_91, + R.drawable.tv_5a_92, + R.drawable.tv_5a_93, + R.drawable.tv_5a_94, + R.drawable.tv_5a_95, + R.drawable.tv_5a_96, + R.drawable.tv_5a_97, + R.drawable.tv_5a_98, + R.drawable.tv_5a_99, + R.drawable.tv_5a_100, + R.drawable.tv_5a_101, + R.drawable.tv_5a_102, + R.drawable.tv_5a_103, + R.drawable.tv_5a_104, + R.drawable.tv_5a_105, + R.drawable.tv_5a_106, + R.drawable.tv_5a_107, + R.drawable.tv_5a_108, + R.drawable.tv_5a_109, + R.drawable.tv_5a_110, + R.drawable.tv_5a_111, + R.drawable.tv_5a_112, + R.drawable.tv_5a_113, + R.drawable.tv_5a_114, + R.drawable.tv_5a_115, + R.drawable.tv_5a_116, + R.drawable.tv_5a_117, + R.drawable.tv_5a_118, + R.drawable.tv_5a_119, + R.drawable.tv_5a_120, + R.drawable.tv_5a_121, + R.drawable.tv_5a_122, + R.drawable.tv_5a_123, + R.drawable.tv_5a_124, + R.drawable.tv_5a_125, + R.drawable.tv_5a_126, + R.drawable.tv_5a_127, + R.drawable.tv_5a_128, + R.drawable.tv_5a_129, + R.drawable.tv_5a_130, + R.drawable.tv_5a_131, + R.drawable.tv_5a_132, + R.drawable.tv_5a_133, + R.drawable.tv_5a_134, + R.drawable.tv_5a_135, + R.drawable.tv_5a_136, + R.drawable.tv_5a_137, + R.drawable.tv_5a_138, + R.drawable.tv_5a_139, + R.drawable.tv_5a_140, + R.drawable.tv_5a_141, + R.drawable.tv_5a_142, + R.drawable.tv_5a_143, + R.drawable.tv_5a_144, + R.drawable.tv_5a_145, + R.drawable.tv_5a_146, + R.drawable.tv_5a_147, + R.drawable.tv_5a_148, + R.drawable.tv_5a_149, + R.drawable.tv_5a_150, + R.drawable.tv_5a_151, + R.drawable.tv_5a_152, + R.drawable.tv_5a_153, + R.drawable.tv_5a_154, + R.drawable.tv_5a_155, + R.drawable.tv_5a_156, + R.drawable.tv_5a_157, + R.drawable.tv_5a_158, + R.drawable.tv_5a_159, + R.drawable.tv_5a_160, + R.drawable.tv_5a_161, + R.drawable.tv_5a_162, + R.drawable.tv_5a_163, + R.drawable.tv_5a_164, + R.drawable.tv_5a_165, + R.drawable.tv_5a_166, + R.drawable.tv_5a_167, + R.drawable.tv_5a_168, + R.drawable.tv_5a_169, + R.drawable.tv_5a_170, + R.drawable.tv_5a_171, + R.drawable.tv_5a_172, + R.drawable.tv_5a_173, + R.drawable.tv_5a_174, + R.drawable.tv_5a_175, + R.drawable.tv_5a_176, + R.drawable.tv_5a_177, + R.drawable.tv_5a_178, + R.drawable.tv_5a_179, + R.drawable.tv_5a_180, + R.drawable.tv_5a_181, + R.drawable.tv_5a_182, + R.drawable.tv_5a_183, + R.drawable.tv_5a_184, + R.drawable.tv_5a_185, + R.drawable.tv_5a_186, + R.drawable.tv_5a_187, + R.drawable.tv_5a_188, + R.drawable.tv_5a_189, + R.drawable.tv_5a_190, + R.drawable.tv_5a_191, + R.drawable.tv_5a_192, + R.drawable.tv_5a_193, + R.drawable.tv_5a_194, + R.drawable.tv_5a_195, + R.drawable.tv_5a_196, + R.drawable.tv_5a_197, + R.drawable.tv_5a_198, + R.drawable.tv_5a_199, + R.drawable.tv_5a_200, + R.drawable.tv_5a_201, + R.drawable.tv_5a_202, + R.drawable.tv_5a_203, + R.drawable.tv_5a_204, + R.drawable.tv_5a_205, + R.drawable.tv_5a_206, + R.drawable.tv_5a_207, + R.drawable.tv_5a_208, + R.drawable.tv_5a_209, + R.drawable.tv_5a_210, + R.drawable.tv_5a_211, + R.drawable.tv_5a_212, + R.drawable.tv_5a_213, + R.drawable.tv_5a_214, + R.drawable.tv_5a_215, + R.drawable.tv_5a_216, + R.drawable.tv_5a_217, + R.drawable.tv_5a_218, + R.drawable.tv_5a_219, + R.drawable.tv_5a_220, + R.drawable.tv_5a_221, + R.drawable.tv_5a_222, + R.drawable.tv_5a_223, + R.drawable.tv_5a_224 }; private static final int[] TV_FRAMES_3_BLUE_ARROW = { - R.drawable.arrow_blue_00, - R.drawable.arrow_blue_01, - R.drawable.arrow_blue_02, - R.drawable.arrow_blue_03, - R.drawable.arrow_blue_04, - R.drawable.arrow_blue_05, - R.drawable.arrow_blue_06, - R.drawable.arrow_blue_07, - R.drawable.arrow_blue_08, - R.drawable.arrow_blue_09, - R.drawable.arrow_blue_10, - R.drawable.arrow_blue_11, - R.drawable.arrow_blue_12, - R.drawable.arrow_blue_13, - R.drawable.arrow_blue_14, - R.drawable.arrow_blue_15, - R.drawable.arrow_blue_16, - R.drawable.arrow_blue_17, - R.drawable.arrow_blue_18, - R.drawable.arrow_blue_19, - R.drawable.arrow_blue_20, - R.drawable.arrow_blue_21, - R.drawable.arrow_blue_22, - R.drawable.arrow_blue_23, - R.drawable.arrow_blue_24, - R.drawable.arrow_blue_25, - R.drawable.arrow_blue_26, - R.drawable.arrow_blue_27, - R.drawable.arrow_blue_28, - R.drawable.arrow_blue_29, - R.drawable.arrow_blue_30, - R.drawable.arrow_blue_31, - R.drawable.arrow_blue_32, - R.drawable.arrow_blue_33, - R.drawable.arrow_blue_34, - R.drawable.arrow_blue_35, - R.drawable.arrow_blue_36, - R.drawable.arrow_blue_37, - R.drawable.arrow_blue_38, - R.drawable.arrow_blue_39, - R.drawable.arrow_blue_40, - R.drawable.arrow_blue_41, - R.drawable.arrow_blue_42, - R.drawable.arrow_blue_43, - R.drawable.arrow_blue_44, - R.drawable.arrow_blue_45, - R.drawable.arrow_blue_46, - R.drawable.arrow_blue_47, - R.drawable.arrow_blue_48, - R.drawable.arrow_blue_49, - R.drawable.arrow_blue_50, - R.drawable.arrow_blue_51, - R.drawable.arrow_blue_52, - R.drawable.arrow_blue_53, - R.drawable.arrow_blue_54, - R.drawable.arrow_blue_55, - R.drawable.arrow_blue_56, - R.drawable.arrow_blue_57, - R.drawable.arrow_blue_58, - R.drawable.arrow_blue_59, - R.drawable.arrow_blue_60 + R.drawable.arrow_blue_00, + R.drawable.arrow_blue_01, + R.drawable.arrow_blue_02, + R.drawable.arrow_blue_03, + R.drawable.arrow_blue_04, + R.drawable.arrow_blue_05, + R.drawable.arrow_blue_06, + R.drawable.arrow_blue_07, + R.drawable.arrow_blue_08, + R.drawable.arrow_blue_09, + R.drawable.arrow_blue_10, + R.drawable.arrow_blue_11, + R.drawable.arrow_blue_12, + R.drawable.arrow_blue_13, + R.drawable.arrow_blue_14, + R.drawable.arrow_blue_15, + R.drawable.arrow_blue_16, + R.drawable.arrow_blue_17, + R.drawable.arrow_blue_18, + R.drawable.arrow_blue_19, + R.drawable.arrow_blue_20, + R.drawable.arrow_blue_21, + R.drawable.arrow_blue_22, + R.drawable.arrow_blue_23, + R.drawable.arrow_blue_24, + R.drawable.arrow_blue_25, + R.drawable.arrow_blue_26, + R.drawable.arrow_blue_27, + R.drawable.arrow_blue_28, + R.drawable.arrow_blue_29, + R.drawable.arrow_blue_30, + R.drawable.arrow_blue_31, + R.drawable.arrow_blue_32, + R.drawable.arrow_blue_33, + R.drawable.arrow_blue_34, + R.drawable.arrow_blue_35, + R.drawable.arrow_blue_36, + R.drawable.arrow_blue_37, + R.drawable.arrow_blue_38, + R.drawable.arrow_blue_39, + R.drawable.arrow_blue_40, + R.drawable.arrow_blue_41, + R.drawable.arrow_blue_42, + R.drawable.arrow_blue_43, + R.drawable.arrow_blue_44, + R.drawable.arrow_blue_45, + R.drawable.arrow_blue_46, + R.drawable.arrow_blue_47, + R.drawable.arrow_blue_48, + R.drawable.arrow_blue_49, + R.drawable.arrow_blue_50, + R.drawable.arrow_blue_51, + R.drawable.arrow_blue_52, + R.drawable.arrow_blue_53, + R.drawable.arrow_blue_54, + R.drawable.arrow_blue_55, + R.drawable.arrow_blue_56, + R.drawable.arrow_blue_57, + R.drawable.arrow_blue_58, + R.drawable.arrow_blue_59, + R.drawable.arrow_blue_60 }; private static final int[] TV_FRAMES_3_BLUE_START = { - R.drawable.tv_2a_01, - R.drawable.tv_2a_02, - R.drawable.tv_2a_03, - R.drawable.tv_2a_04, - R.drawable.tv_2a_05, - R.drawable.tv_2a_06, - R.drawable.tv_2a_07, - R.drawable.tv_2a_08, - R.drawable.tv_2a_09, - R.drawable.tv_2a_10, - R.drawable.tv_2a_11, - R.drawable.tv_2a_12, - R.drawable.tv_2a_13, - R.drawable.tv_2a_14, - R.drawable.tv_2a_15, - R.drawable.tv_2a_16, - R.drawable.tv_2a_17, - R.drawable.tv_2a_18, - R.drawable.tv_2a_19 + R.drawable.tv_2a_01, + R.drawable.tv_2a_02, + R.drawable.tv_2a_03, + R.drawable.tv_2a_04, + R.drawable.tv_2a_05, + R.drawable.tv_2a_06, + R.drawable.tv_2a_07, + R.drawable.tv_2a_08, + R.drawable.tv_2a_09, + R.drawable.tv_2a_10, + R.drawable.tv_2a_11, + R.drawable.tv_2a_12, + R.drawable.tv_2a_13, + R.drawable.tv_2a_14, + R.drawable.tv_2a_15, + R.drawable.tv_2a_16, + R.drawable.tv_2a_17, + R.drawable.tv_2a_18, + R.drawable.tv_2a_19 }; private static final int[] TV_FRAMES_3_BLUE_END = { - R.drawable.tv_2b_01, - R.drawable.tv_2b_02, - R.drawable.tv_2b_03, - R.drawable.tv_2b_04, - R.drawable.tv_2b_05, - R.drawable.tv_2b_06, - R.drawable.tv_2b_07, - R.drawable.tv_2b_08, - R.drawable.tv_2b_09, - R.drawable.tv_2b_10, - R.drawable.tv_2b_11, - R.drawable.tv_2b_12, - R.drawable.tv_2b_13, - R.drawable.tv_2b_14, - R.drawable.tv_2b_15, - R.drawable.tv_2b_16, - R.drawable.tv_2b_17, - R.drawable.tv_2b_18, - R.drawable.tv_2b_19 + R.drawable.tv_2b_01, + R.drawable.tv_2b_02, + R.drawable.tv_2b_03, + R.drawable.tv_2b_04, + R.drawable.tv_2b_05, + R.drawable.tv_2b_06, + R.drawable.tv_2b_07, + R.drawable.tv_2b_08, + R.drawable.tv_2b_09, + R.drawable.tv_2b_10, + R.drawable.tv_2b_11, + R.drawable.tv_2b_12, + R.drawable.tv_2b_13, + R.drawable.tv_2b_14, + R.drawable.tv_2b_15, + R.drawable.tv_2b_16, + R.drawable.tv_2b_17, + R.drawable.tv_2b_18, + R.drawable.tv_2b_19 }; private static final int[] TV_FRAMES_3_ORANGE_ARROW = { - R.drawable.arrow_orange_180, - R.drawable.arrow_orange_181, - R.drawable.arrow_orange_182, - R.drawable.arrow_orange_183, - R.drawable.arrow_orange_184, - R.drawable.arrow_orange_185, - R.drawable.arrow_orange_186, - R.drawable.arrow_orange_187, - R.drawable.arrow_orange_188, - R.drawable.arrow_orange_189, - R.drawable.arrow_orange_190, - R.drawable.arrow_orange_191, - R.drawable.arrow_orange_192, - R.drawable.arrow_orange_193, - R.drawable.arrow_orange_194, - R.drawable.arrow_orange_195, - R.drawable.arrow_orange_196, - R.drawable.arrow_orange_197, - R.drawable.arrow_orange_198, - R.drawable.arrow_orange_199, - R.drawable.arrow_orange_200, - R.drawable.arrow_orange_201, - R.drawable.arrow_orange_202, - R.drawable.arrow_orange_203, - R.drawable.arrow_orange_204, - R.drawable.arrow_orange_205, - R.drawable.arrow_orange_206, - R.drawable.arrow_orange_207, - R.drawable.arrow_orange_208, - R.drawable.arrow_orange_209, - R.drawable.arrow_orange_210, - R.drawable.arrow_orange_211, - R.drawable.arrow_orange_212, - R.drawable.arrow_orange_213, - R.drawable.arrow_orange_214, - R.drawable.arrow_orange_215, - R.drawable.arrow_orange_216, - R.drawable.arrow_orange_217, - R.drawable.arrow_orange_218, - R.drawable.arrow_orange_219, - R.drawable.arrow_orange_220, - R.drawable.arrow_orange_221, - R.drawable.arrow_orange_222, - R.drawable.arrow_orange_223, - R.drawable.arrow_orange_224, - R.drawable.arrow_orange_225, - R.drawable.arrow_orange_226, - R.drawable.arrow_orange_227, - R.drawable.arrow_orange_228, - R.drawable.arrow_orange_229, - R.drawable.arrow_orange_230, - R.drawable.arrow_orange_231, - R.drawable.arrow_orange_232, - R.drawable.arrow_orange_233, - R.drawable.arrow_orange_234, - R.drawable.arrow_orange_235, - R.drawable.arrow_orange_236, - R.drawable.arrow_orange_237, - R.drawable.arrow_orange_238, - R.drawable.arrow_orange_239, - R.drawable.arrow_orange_240 + R.drawable.arrow_orange_180, + R.drawable.arrow_orange_181, + R.drawable.arrow_orange_182, + R.drawable.arrow_orange_183, + R.drawable.arrow_orange_184, + R.drawable.arrow_orange_185, + R.drawable.arrow_orange_186, + R.drawable.arrow_orange_187, + R.drawable.arrow_orange_188, + R.drawable.arrow_orange_189, + R.drawable.arrow_orange_190, + R.drawable.arrow_orange_191, + R.drawable.arrow_orange_192, + R.drawable.arrow_orange_193, + R.drawable.arrow_orange_194, + R.drawable.arrow_orange_195, + R.drawable.arrow_orange_196, + R.drawable.arrow_orange_197, + R.drawable.arrow_orange_198, + R.drawable.arrow_orange_199, + R.drawable.arrow_orange_200, + R.drawable.arrow_orange_201, + R.drawable.arrow_orange_202, + R.drawable.arrow_orange_203, + R.drawable.arrow_orange_204, + R.drawable.arrow_orange_205, + R.drawable.arrow_orange_206, + R.drawable.arrow_orange_207, + R.drawable.arrow_orange_208, + R.drawable.arrow_orange_209, + R.drawable.arrow_orange_210, + R.drawable.arrow_orange_211, + R.drawable.arrow_orange_212, + R.drawable.arrow_orange_213, + R.drawable.arrow_orange_214, + R.drawable.arrow_orange_215, + R.drawable.arrow_orange_216, + R.drawable.arrow_orange_217, + R.drawable.arrow_orange_218, + R.drawable.arrow_orange_219, + R.drawable.arrow_orange_220, + R.drawable.arrow_orange_221, + R.drawable.arrow_orange_222, + R.drawable.arrow_orange_223, + R.drawable.arrow_orange_224, + R.drawable.arrow_orange_225, + R.drawable.arrow_orange_226, + R.drawable.arrow_orange_227, + R.drawable.arrow_orange_228, + R.drawable.arrow_orange_229, + R.drawable.arrow_orange_230, + R.drawable.arrow_orange_231, + R.drawable.arrow_orange_232, + R.drawable.arrow_orange_233, + R.drawable.arrow_orange_234, + R.drawable.arrow_orange_235, + R.drawable.arrow_orange_236, + R.drawable.arrow_orange_237, + R.drawable.arrow_orange_238, + R.drawable.arrow_orange_239, + R.drawable.arrow_orange_240 }; private static final int[] TV_FRAMES_3_ORANGE_START = { - R.drawable.tv_2c_01, - R.drawable.tv_2c_02, - R.drawable.tv_2c_03, - R.drawable.tv_2c_04, - R.drawable.tv_2c_05, - R.drawable.tv_2c_06, - R.drawable.tv_2c_07, - R.drawable.tv_2c_08, - R.drawable.tv_2c_09, - R.drawable.tv_2c_10, - R.drawable.tv_2c_11, - R.drawable.tv_2c_12, - R.drawable.tv_2c_13, - R.drawable.tv_2c_14, - R.drawable.tv_2c_15, - R.drawable.tv_2c_16 + R.drawable.tv_2c_01, + R.drawable.tv_2c_02, + R.drawable.tv_2c_03, + R.drawable.tv_2c_04, + R.drawable.tv_2c_05, + R.drawable.tv_2c_06, + R.drawable.tv_2c_07, + R.drawable.tv_2c_08, + R.drawable.tv_2c_09, + R.drawable.tv_2c_10, + R.drawable.tv_2c_11, + R.drawable.tv_2c_12, + R.drawable.tv_2c_13, + R.drawable.tv_2c_14, + R.drawable.tv_2c_15, + R.drawable.tv_2c_16 }; private static final int[] TV_FRAMES_4_START = { - R.drawable.tv_3a_01, - R.drawable.tv_3a_02, - R.drawable.tv_3a_03, - R.drawable.tv_3a_04, - R.drawable.tv_3a_05, - R.drawable.tv_3a_06, - R.drawable.tv_3a_07, - R.drawable.tv_3a_08, - R.drawable.tv_3a_09, - R.drawable.tv_3a_10, - R.drawable.tv_3a_11, - R.drawable.tv_3a_12, - R.drawable.tv_3a_13, - R.drawable.tv_3a_14, - R.drawable.tv_3a_15, - R.drawable.tv_3a_16, - R.drawable.tv_3a_17, - R.drawable.tv_3b_75, - R.drawable.tv_3b_76, - R.drawable.tv_3b_77, - R.drawable.tv_3b_78, - R.drawable.tv_3b_79, - R.drawable.tv_3b_80, - R.drawable.tv_3b_81, - R.drawable.tv_3b_82, - R.drawable.tv_3b_83, - R.drawable.tv_3b_84, - R.drawable.tv_3b_85, - R.drawable.tv_3b_86, - R.drawable.tv_3b_87, - R.drawable.tv_3b_88, - R.drawable.tv_3b_89, - R.drawable.tv_3b_90, - R.drawable.tv_3b_91, - R.drawable.tv_3b_92, - R.drawable.tv_3b_93, - R.drawable.tv_3b_94, - R.drawable.tv_3b_95, - R.drawable.tv_3b_96, - R.drawable.tv_3b_97, - R.drawable.tv_3b_98, - R.drawable.tv_3b_99, - R.drawable.tv_3b_100, - R.drawable.tv_3b_101, - R.drawable.tv_3b_102, - R.drawable.tv_3b_103, - R.drawable.tv_3b_104, - R.drawable.tv_3b_105, - R.drawable.tv_3b_106, - R.drawable.tv_3b_107, - R.drawable.tv_3b_108, - R.drawable.tv_3b_109, - R.drawable.tv_3b_110, - R.drawable.tv_3b_111, - R.drawable.tv_3b_112, - R.drawable.tv_3b_113, - R.drawable.tv_3b_114, - R.drawable.tv_3b_115, - R.drawable.tv_3b_116, - R.drawable.tv_3b_117, - R.drawable.tv_3b_118 + R.drawable.tv_3a_01, + R.drawable.tv_3a_02, + R.drawable.tv_3a_03, + R.drawable.tv_3a_04, + R.drawable.tv_3a_05, + R.drawable.tv_3a_06, + R.drawable.tv_3a_07, + R.drawable.tv_3a_08, + R.drawable.tv_3a_09, + R.drawable.tv_3a_10, + R.drawable.tv_3a_11, + R.drawable.tv_3a_12, + R.drawable.tv_3a_13, + R.drawable.tv_3a_14, + R.drawable.tv_3a_15, + R.drawable.tv_3a_16, + R.drawable.tv_3a_17, + R.drawable.tv_3b_75, + R.drawable.tv_3b_76, + R.drawable.tv_3b_77, + R.drawable.tv_3b_78, + R.drawable.tv_3b_79, + R.drawable.tv_3b_80, + R.drawable.tv_3b_81, + R.drawable.tv_3b_82, + R.drawable.tv_3b_83, + R.drawable.tv_3b_84, + R.drawable.tv_3b_85, + R.drawable.tv_3b_86, + R.drawable.tv_3b_87, + R.drawable.tv_3b_88, + R.drawable.tv_3b_89, + R.drawable.tv_3b_90, + R.drawable.tv_3b_91, + R.drawable.tv_3b_92, + R.drawable.tv_3b_93, + R.drawable.tv_3b_94, + R.drawable.tv_3b_95, + R.drawable.tv_3b_96, + R.drawable.tv_3b_97, + R.drawable.tv_3b_98, + R.drawable.tv_3b_99, + R.drawable.tv_3b_100, + R.drawable.tv_3b_101, + R.drawable.tv_3b_102, + R.drawable.tv_3b_103, + R.drawable.tv_3b_104, + R.drawable.tv_3b_105, + R.drawable.tv_3b_106, + R.drawable.tv_3b_107, + R.drawable.tv_3b_108, + R.drawable.tv_3b_109, + R.drawable.tv_3b_110, + R.drawable.tv_3b_111, + R.drawable.tv_3b_112, + R.drawable.tv_3b_113, + R.drawable.tv_3b_114, + R.drawable.tv_3b_115, + R.drawable.tv_3b_116, + R.drawable.tv_3b_117, + R.drawable.tv_3b_118 }; private String[] mPageTitles; @@ -581,13 +585,21 @@ public class WelcomeFragment extends OnboardingFragment { private ImageView mTvContentView; private ImageView mArrowView; + private TextView mTitleView; + private Button mStartButton; + private View mPagingIndicator; + private Animator mAnimator; + private boolean mLogoAnimationFinished; + private boolean mTitleChanged; + public WelcomeFragment() { - setExitTransition(new SetupAnimationHelper.TransitionBuilder() - .setSlideEdge(Gravity.START) - .setParentIdsForDelay(new int[]{R.id.onboarding_fragment_root}) - .build()); + setExitTransition( + new SetupAnimationHelper.TransitionBuilder() + .setSlideEdge(Gravity.START) + .setParentIdsForDelay(new int[] {R.id.onboarding_fragment_root}) + .build()); } @Override @@ -605,11 +617,97 @@ public class WelcomeFragment extends OnboardingFragment { @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); setLogoResourceId(R.drawable.splash_logo); - if (savedInstanceState != null) { + mTitleView = view.findViewById(android.support.v17.leanback.R.id.title); + mPagingIndicator = view.findViewById(android.support.v17.leanback.R.id.page_indicator); + mStartButton = view.findViewById(android.support.v17.leanback.R.id.button_start); + + mStartButton.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent( + View host, AccessibilityEvent event) { + int type = event.getEventType(); + if (type == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + || type == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + if (!mTitleChanged || mTitleView.isAccessibilityFocused()) { + // Skip the event before the title is accessibility focused to avoid + // race + // conditions + return; + } + } + super.onInitializeAccessibilityEvent(host, event); + } + }); + + mPagingIndicator.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityEvent( + View host, AccessibilityEvent event) { + int type = event.getEventType(); + if (type == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + || type == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + if (!mTitleChanged || mTitleView.isAccessibilityFocused()) { + // Skip the event before the title is accessibility focused to avoid + // race + // conditions + return; + } + } + super.onInitializeAccessibilityEvent(host, event); + } + }); + + mTitleView.setAccessibilityDelegate( + new AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) { + if (!mTitleChanged || mTitleView.isAccessibilityFocused()) { + // Skip the event before the title is accessibility focused to avoid + // race + // conditions + return false; + } + } + return super.performAccessibilityAction(host, action, args); + } + }); + + mTitleView.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + mTitleChanged = false; + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (!mTitleView.isAccessibilityFocused()) { + mTitleView.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } else { + mTitleView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + } + mTitleChanged = true; + } + }); + return view; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (savedInstanceState != null && mLogoAnimationFinished) { switch (getCurrentPageIndex()) { case 0: mTvContentView.setImageResource( @@ -631,7 +729,6 @@ public class WelcomeFragment extends OnboardingFragment { break; } } - return view; } @Override @@ -640,29 +737,38 @@ public class WelcomeFragment extends OnboardingFragment { } @Override + protected void onLogoAnimationFinished() { + super.onLogoAnimationFinished(); + mLogoAnimationFinished = true; + } + + @Override protected Animator onCreateEnterAnimation() { List<Animator> animators = new ArrayList<>(); // Cloud 1 View view = getActivity().findViewById(R.id.cloud1); view.setAlpha(0); - Animator animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.onboarding_welcome_cloud_enter); + Animator animator = + AnimatorInflater.loadAnimator( + getActivity(), R.animator.onboarding_welcome_cloud_enter); animator.setStartDelay(START_DELAY_CLOUD_MS); animator.setTarget(view); animators.add(animator); // Cloud 2 view = getActivity().findViewById(R.id.cloud2); view.setAlpha(0); - animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.onboarding_welcome_cloud_enter); + animator = + AnimatorInflater.loadAnimator( + getActivity(), R.animator.onboarding_welcome_cloud_enter); animator.setStartDelay(START_DELAY_CLOUD_MS); animator.setTarget(view); animators.add(animator); // TV container view = getActivity().findViewById(R.id.tv_container); view.setAlpha(0); - animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.onboarding_welcome_tv_enter); + animator = + AnimatorInflater.loadAnimator( + getActivity(), R.animator.onboarding_welcome_tv_enter); animator.setStartDelay(START_DELAY_TV_MS); animator.setTarget(view); animators.add(animator); @@ -675,8 +781,9 @@ public class WelcomeFragment extends OnboardingFragment { // Shadow view = getActivity().findViewById(R.id.shadow); view.setAlpha(0); - animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.onboarding_welcome_shadow_enter); + animator = + AnimatorInflater.loadAnimator( + getActivity(), R.animator.onboarding_welcome_shadow_enter); animator.setStartDelay(START_DELAY_SHADOW_MS); animator.setTarget(view); animators.add(animator); @@ -702,8 +809,9 @@ public class WelcomeFragment extends OnboardingFragment { @Nullable @Override protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container) { - mArrowView = (ImageView) inflater.inflate(R.layout.onboarding_welcome_foreground, container, - false); + mArrowView = + (ImageView) + inflater.inflate(R.layout.onboarding_welcome_foreground, container, false); return mArrowView; } @@ -732,65 +840,74 @@ public class WelcomeFragment extends OnboardingFragment { if (mAnimator != null) { mAnimator.cancel(); } + mTitleChanged = false; mArrowView.setVisibility(View.GONE); // TV screen hiding animator. - Animator hideAnimator = previousPage == 0 - ? SetupAnimationHelper.createFrameAnimator(mTvContentView, TV_FRAMES_1_END) - : SetupAnimationHelper.createFadeOutAnimator(mTvContentView, - VIDEO_FADE_OUT_DURATION_MS, true); + Animator hideAnimator = + previousPage == 0 + ? SetupAnimationHelper.createFrameAnimator(mTvContentView, TV_FRAMES_1_END) + : SetupAnimationHelper.createFadeOutAnimator( + mTvContentView, VIDEO_FADE_OUT_DURATION_MS, true); // TV screen showing animator. AnimatorSet animatorSet = new AnimatorSet(); int firstFrame; switch (newPage) { case 0: - animatorSet.playSequentially(hideAnimator, - SetupAnimationHelper.createFrameAnimator(mTvContentView, - TV_FRAMES_1_START)); + animatorSet.playSequentially( + hideAnimator, + SetupAnimationHelper.createFrameAnimator( + mTvContentView, TV_FRAMES_1_START)); firstFrame = TV_FRAMES_1_START[0]; break; case 1: - animatorSet.playSequentially(hideAnimator, - SetupAnimationHelper.createFrameAnimator(mTvContentView, - TV_FRAMES_2_START)); + animatorSet.playSequentially( + hideAnimator, + SetupAnimationHelper.createFrameAnimator( + mTvContentView, TV_FRAMES_2_START)); firstFrame = TV_FRAMES_2_START[0]; break; case 2: mArrowView.setVisibility(View.VISIBLE); - animatorSet.playSequentially(hideAnimator, - SetupAnimationHelper.createFrameAnimator(mArrowView, - TV_FRAMES_3_BLUE_ARROW), - SetupAnimationHelper.createFrameAnimator(mTvContentView, - TV_FRAMES_3_BLUE_START), - SetupAnimationHelper.createFrameAnimatorWithDelay(mTvContentView, - TV_FRAMES_3_BLUE_END, BLUE_SCREEN_HOLD_DURATION_MS), - SetupAnimationHelper.createFrameAnimator(mArrowView, - TV_FRAMES_3_ORANGE_ARROW), - SetupAnimationHelper.createFrameAnimator(mTvContentView, - TV_FRAMES_3_ORANGE_START)); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mArrowView.setImageResource(TV_FRAMES_3_BLUE_ARROW[0]); - } - }); + animatorSet.playSequentially( + hideAnimator, + SetupAnimationHelper.createFrameAnimator( + mArrowView, TV_FRAMES_3_BLUE_ARROW), + SetupAnimationHelper.createFrameAnimator( + mTvContentView, TV_FRAMES_3_BLUE_START), + SetupAnimationHelper.createFrameAnimatorWithDelay( + mTvContentView, TV_FRAMES_3_BLUE_END, BLUE_SCREEN_HOLD_DURATION_MS), + SetupAnimationHelper.createFrameAnimator( + mArrowView, TV_FRAMES_3_ORANGE_ARROW), + SetupAnimationHelper.createFrameAnimator( + mTvContentView, TV_FRAMES_3_ORANGE_START)); + animatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mArrowView.setImageResource(TV_FRAMES_3_BLUE_ARROW[0]); + } + }); firstFrame = TV_FRAMES_3_BLUE_START[0]; break; case 3: default: - animatorSet.playSequentially(hideAnimator, - SetupAnimationHelper.createFrameAnimator(mTvContentView, - TV_FRAMES_4_START)); + animatorSet.playSequentially( + hideAnimator, + SetupAnimationHelper.createFrameAnimator( + mTvContentView, TV_FRAMES_4_START)); firstFrame = TV_FRAMES_4_START[0]; break; } final int firstImageResource = firstFrame; - hideAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Shows the first frame of show animation when the hide animator is canceled. - mTvContentView.setImageResource(firstImageResource); - } - }); + hideAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Shows the first frame of show animation when the hide animator is + // canceled. + mTvContentView.setImageResource(firstImageResource); + } + }); mAnimator = SetupAnimationHelper.applyAnimationTimeScale(animatorSet); mAnimator.start(); } diff --git a/src/com/android/tv/parental/ContentRatingLevelPolicy.java b/src/com/android/tv/parental/ContentRatingLevelPolicy.java index 9bd15423..5b0a6cf5 100644 --- a/src/com/android/tv/parental/ContentRatingLevelPolicy.java +++ b/src/com/android/tv/parental/ContentRatingLevelPolicy.java @@ -17,12 +17,10 @@ package com.android.tv.parental; import android.media.tv.TvContentRating; - import com.android.tv.parental.ContentRatingSystem.Rating; import com.android.tv.parental.ContentRatingSystem.SubRating; import com.android.tv.util.TvSettings; import com.android.tv.util.TvSettings.ContentRatingLevel; - import java.util.HashSet; import java.util.Set; @@ -31,10 +29,11 @@ public class ContentRatingLevelPolicy { private static final int AGE_THRESHOLD_FOR_LEVEL_MEDIUM = 12; private static final int AGE_THRESHOLD_FOR_LEVEL_LOW = -1; // Highest age for each rating system - private ContentRatingLevelPolicy() { } + private ContentRatingLevelPolicy() {} public static Set<TvContentRating> getRatingsForLevel( - ParentalControlSettings settings, ContentRatingsManager manager, + ParentalControlSettings settings, + ContentRatingsManager manager, @ContentRatingLevel int level) { if (level == TvSettings.CONTENT_RATING_LEVEL_NONE) { return new HashSet<>(); @@ -64,14 +63,17 @@ public class ContentRatingLevelPolicy { if (rating.getAgeHint() < ageLimit) { continue; } - TvContentRating tvContentRating = TvContentRating.createRating( - contentRatingSystem.getDomain(), contentRatingSystem.getName(), - rating.getName()); + TvContentRating tvContentRating = + TvContentRating.createRating( + contentRatingSystem.getDomain(), + contentRatingSystem.getName(), + rating.getName()); ratings.add(tvContentRating); for (SubRating subRating : rating.getSubRatings()) { - tvContentRating = TvContentRating.createRating( - contentRatingSystem.getDomain(), contentRatingSystem.getName(), - rating.getName(), subRating.getName()); + tvContentRating = + TvContentRating.createRating( + contentRatingSystem.getDomain(), contentRatingSystem.getName(), + rating.getName(), subRating.getName()); ratings.add(tvContentRating); } } diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java index 5672b793..600aaca1 100644 --- a/src/com/android/tv/parental/ContentRatingSystem.java +++ b/src/com/android/tv/parental/ContentRatingSystem.java @@ -20,9 +20,7 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.media.tv.TvContentRating; import android.text.TextUtils; - import com.android.tv.R; - import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -86,7 +84,7 @@ public class ContentRatingSystem { return mDomain + DELIMITER + mName; } - public String getName(){ + public String getName() { return mName; } @@ -94,19 +92,19 @@ public class ContentRatingSystem { return mDomain; } - public String getTitle(){ + public String getTitle() { return mTitle; } - public String getDescription(){ + public String getDescription() { return mDescription; } - public List<String> getCountries(){ + public List<String> getCountries() { return mCountries; } - public List<Rating> getRatings(){ + public List<Rating> getRatings() { return mRatings; } @@ -119,11 +117,11 @@ public class ContentRatingSystem { return null; } - public List<SubRating> getSubRatings(){ + public List<SubRating> getSubRatings() { return mSubRatings; } - public List<Order> getOrders(){ + public List<Order> getOrders() { return mOrders; } @@ -139,9 +137,7 @@ public class ContentRatingSystem { return mIsCustom; } - /** - * Returns true if the ratings is owned by this content rating system. - */ + /** Returns true if the ratings is owned by this content rating system. */ public boolean ownsRating(TvContentRating rating) { return mDomain.equals(rating.getDomain()) && mName.equals(rating.getRatingSystem()); } @@ -161,9 +157,16 @@ public class ContentRatingSystem { } private ContentRatingSystem( - String name, String domain, String title, String description, List<String> countries, - String displayName, List<Rating> ratings, List<SubRating> subRatings, - List<Order> orders, boolean isCustom) { + String name, + String domain, + String title, + String description, + List<String> countries, + String displayName, + List<Rating> ratings, + List<SubRating> subRatings, + List<Order> orders, + boolean isCustom) { mName = name; mDomain = domain; mTitle = title; @@ -298,8 +301,8 @@ public class ContentRatingSystem { } } if (!used) { - throw new IllegalArgumentException("Subrating " + subRating.getName() + - " isn't used by any rating"); + throw new IllegalArgumentException( + "Subrating " + subRating.getName() + " isn't used by any rating"); } } @@ -310,8 +313,17 @@ public class ContentRatingSystem { } } - return new ContentRatingSystem(mName, mDomain, mTitle, mDescription, mCountries, - displayName, ratings, subRatings, orders, mIsCustom); + return new ContentRatingSystem( + mName, + mDomain, + mTitle, + mDescription, + mCountries, + displayName, + ratings, + subRatings, + orders, + mIsCustom); } } @@ -347,8 +359,13 @@ public class ContentRatingSystem { return mSubRatings; } - private Rating(String name, String title, String description, Drawable icon, - int contentAgeHint, List<SubRating> subRatings) { + private Rating( + String name, + String title, + String description, + Drawable icon, + int contentAgeHint, + List<SubRating> subRatings) { mName = name; mTitle = title; mDescription = description; @@ -365,8 +382,7 @@ public class ContentRatingSystem { private int mContentAgeHint = -1; private final List<String> mSubRatingNames = new ArrayList<>(); - public Builder() { - } + public Builder() {} public void setName(String name) { mName = name; @@ -400,8 +416,8 @@ public class ContentRatingSystem { throw new IllegalArgumentException("Invalid subrating for rating " + mName); } if (mContentAgeHint < 0) { - throw new IllegalArgumentException("Rating " + mName + " should define " + - "non-negative contentAgeHint"); + throw new IllegalArgumentException( + "Rating " + mName + " should define " + "non-negative contentAgeHint"); } List<SubRating> subRatings = new ArrayList<>(); @@ -415,12 +431,11 @@ public class ContentRatingSystem { } } if (!found) { - throw new IllegalArgumentException("Unknown subrating name " + subRatingId + - " in rating " + mName); + throw new IllegalArgumentException( + "Unknown subrating name " + subRatingId + " in rating " + mName); } } - return new Rating( - mName, mTitle, mDescription, mIcon, mContentAgeHint, subRatings); + return new Rating(mName, mTitle, mDescription, mIcon, mContentAgeHint, subRatings); } } } @@ -460,8 +475,7 @@ public class ContentRatingSystem { private String mDescription; private Drawable mIcon; - public Builder() { - } + public Builder() {} public void setName(String name) { mName = name; @@ -500,8 +514,8 @@ public class ContentRatingSystem { } /** - * Returns index of the rating in this order. - * Returns -1 if this order doesn't contain the rating. + * Returns index of the rating in this order. Returns -1 if this order doesn't contain the + * rating. */ public int getRatingIndex(Rating rating) { for (int i = 0; i < mRatingOrder.size(); i++) { @@ -515,8 +529,7 @@ public class ContentRatingSystem { public static class Builder { private final List<String> mRatingNames = new ArrayList<>(); - public Builder() { - } + public Builder() {} private Order build(List<Rating> ratings) { List<Rating> ratingOrder = new ArrayList<>(); @@ -531,8 +544,8 @@ public class ContentRatingSystem { } if (!found) { - throw new IllegalArgumentException("Unknown rating " + ratingName + - " in rating-order tag"); + throw new IllegalArgumentException( + "Unknown rating " + ratingName + " in rating-order tag"); } } diff --git a/src/com/android/tv/parental/ContentRatingsManager.java b/src/com/android/tv/parental/ContentRatingsManager.java index c3bb8c0f..32a1325b 100644 --- a/src/com/android/tv/parental/ContentRatingsManager.java +++ b/src/com/android/tv/parental/ContentRatingsManager.java @@ -19,14 +19,12 @@ package com.android.tv.parental; import android.content.Context; import android.media.tv.TvContentRating; import android.media.tv.TvContentRatingSystemInfo; -import android.media.tv.TvInputManager; import android.support.annotation.Nullable; import android.text.TextUtils; - import com.android.tv.R; import com.android.tv.parental.ContentRatingSystem.Rating; import com.android.tv.parental.ContentRatingSystem.SubRating; - +import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.List; @@ -34,19 +32,19 @@ public class ContentRatingsManager { private final List<ContentRatingSystem> mContentRatingSystems = new ArrayList<>(); private final Context mContext; + private final TvInputManagerHelper.TvInputManagerInterface mTvInputManager; - public ContentRatingsManager(Context context) { + public ContentRatingsManager( + Context context, TvInputManagerHelper.TvInputManagerInterface tvInputManager) { mContext = context; + this.mTvInputManager = tvInputManager; } public void update() { mContentRatingSystems.clear(); - - TvInputManager manager = - (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE); ContentRatingsParser parser = new ContentRatingsParser(mContext); - List<TvContentRatingSystemInfo> infos = manager.getTvContentRatingSystemList(); + List<TvContentRatingSystemInfo> infos = mTvInputManager.getTvContentRatingSystemList(); for (TvContentRatingSystemInfo info : infos) { List<ContentRatingSystem> list = parser.parse(info); if (list != null) { @@ -55,9 +53,7 @@ public class ContentRatingsManager { } } - /** - * Returns the content rating system with the give ID. - */ + /** Returns the content rating system with the give ID. */ @Nullable public ContentRatingSystem getContentRatingSystem(String contentRatingSystemId) { for (ContentRatingSystem ratingSystem : mContentRatingSystems) { @@ -68,9 +64,7 @@ public class ContentRatingsManager { return null; } - /** - * Returns a new list of all content rating systems defined. - */ + /** Returns a new list of all content rating systems defined. */ public List<ContentRatingSystem> getContentRatingSystems() { return new ArrayList<>(mContentRatingSystems); } @@ -118,8 +112,10 @@ public class ContentRatingsManager { private List<SubRating> getSubRatings(Rating rating, TvContentRating canonicalRating) { List<SubRating> subRatings = new ArrayList<>(); - if (rating == null || rating.getSubRatings() == null - || canonicalRating == null || canonicalRating.getSubRatings() == null) { + if (rating == null + || rating.getSubRatings() == null + || canonicalRating == null + || canonicalRating.getSubRatings() == null) { return subRatings; } for (String subRatingString : canonicalRating.getSubRatings()) { diff --git a/src/com/android/tv/parental/ContentRatingsParser.java b/src/com/android/tv/parental/ContentRatingsParser.java index d9f62473..14df88ea 100644 --- a/src/com/android/tv/parental/ContentRatingsParser.java +++ b/src/com/android/tv/parental/ContentRatingsParser.java @@ -24,18 +24,16 @@ import android.content.res.XmlResourceParser; import android.media.tv.TvContentRatingSystemInfo; import android.net.Uri; import android.util.Log; - import com.android.tv.parental.ContentRatingSystem.Order; import com.android.tv.parental.ContentRatingSystem.Rating; import com.android.tv.parental.ContentRatingSystem.SubRating; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +/** Parses Content Ratings */ public class ContentRatingsParser { private static final String TAG = "ContentRatingsParser"; private static final boolean DEBUG = false; @@ -74,8 +72,8 @@ public class ContentRatingsParser { try { String packageName = uri.getAuthority(); int resId = (int) ContentUris.parseId(uri); - try (XmlResourceParser parser = mContext.getPackageManager() - .getXml(packageName, resId, null)) { + try (XmlResourceParser parser = + mContext.getPackageManager().getXml(packageName, resId, null)) { if (parser == null) { throw new IllegalArgumentException("Cannot get XML with URI " + uri); } @@ -90,8 +88,8 @@ public class ContentRatingsParser { return ratingSystems; } - private List<ContentRatingSystem> parse(XmlResourceParser parser, String domain, - boolean isCustom) + private List<ContentRatingSystem> parse( + XmlResourceParser parser, String domain, boolean isCustom) throws XmlPullParserException, IOException { try { mResources = mContext.getPackageManager().getResourcesForApplication(domain); @@ -112,7 +110,9 @@ public class ContentRatingsParser { int eventType = parser.getEventType(); assertEquals(eventType, XmlPullParser.START_TAG, "Malformed XML: Not a valid XML file"); - assertEquals(parser.getName(), TAG_RATING_SYSTEM_DEFINITIONS, + assertEquals( + parser.getName(), + TAG_RATING_SYSTEM_DEFINITIONS, "Malformed XML: Should start with tag " + TAG_RATING_SYSTEM_DEFINITIONS); boolean hasVersionAttr = false; @@ -124,8 +124,10 @@ public class ContentRatingsParser { } } if (!hasVersionAttr) { - throw new XmlPullParserException("Malformed XML: Should contains a version attribute" - + " in " + TAG_RATING_SYSTEM_DEFINITIONS); + throw new XmlPullParserException( + "Malformed XML: Should contains a version attribute" + + " in " + + TAG_RATING_SYSTEM_DEFINITIONS); } List<ContentRatingSystem> ratingSystems = new ArrayList<>(); @@ -135,25 +137,29 @@ public class ContentRatingsParser { if (TAG_RATING_SYSTEM_DEFINITION.equals(parser.getName())) { ratingSystems.add(parseRatingSystemDefinition(parser, domain, isCustom)); } else { - checkVersion("Malformed XML: Should contains " + - TAG_RATING_SYSTEM_DEFINITION); + checkVersion( + "Malformed XML: Should contains " + TAG_RATING_SYSTEM_DEFINITION); } break; case XmlPullParser.END_TAG: if (TAG_RATING_SYSTEM_DEFINITIONS.equals(parser.getName())) { eventType = parser.next(); - assertEquals(eventType, XmlPullParser.END_DOCUMENT, - "Malformed XML: Should end with tag " + - TAG_RATING_SYSTEM_DEFINITIONS); + assertEquals( + eventType, + XmlPullParser.END_DOCUMENT, + "Malformed XML: Should end with tag " + + TAG_RATING_SYSTEM_DEFINITIONS); return ratingSystems; } else { - checkVersion("Malformed XML: Should end with tag " + - TAG_RATING_SYSTEM_DEFINITIONS); + checkVersion( + "Malformed XML: Should end with tag " + + TAG_RATING_SYSTEM_DEFINITIONS); } } } - throw new XmlPullParserException(TAG_RATING_SYSTEM_DEFINITIONS + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_RATING_SYSTEM_DEFINITIONS + + " section is incomplete or section ending tag is missing"); } private static void assertEquals(int a, int b, String msg) throws XmlPullParserException { @@ -174,8 +180,9 @@ public class ContentRatingsParser { } } - private ContentRatingSystem parseRatingSystemDefinition(XmlResourceParser parser, String domain, - boolean isCustom) throws XmlPullParserException, IOException { + private ContentRatingSystem parseRatingSystemDefinition( + XmlResourceParser parser, String domain, boolean isCustom) + throws XmlPullParserException, IOException { ContentRatingSystem.Builder builder = new ContentRatingSystem.Builder(mContext); builder.setDomain(domain); @@ -198,13 +205,17 @@ public class ContentRatingsParser { mResources.getString(parser.getAttributeResourceValue(i, 0))); break; default: - checkVersion("Malformed XML: Unknown attribute " + attr + " in " + - TAG_RATING_SYSTEM_DEFINITION); + checkVersion( + "Malformed XML: Unknown attribute " + + attr + + " in " + + TAG_RATING_SYSTEM_DEFINITION); } } while (parser.next() != XmlPullParser.END_DOCUMENT) { - switch (parser.getEventType()) { + int eventType = parser.getEventType(); + switch (eventType) { case XmlPullParser.START_TAG: String tag = parser.getName(); switch (tag) { @@ -218,8 +229,11 @@ public class ContentRatingsParser { builder.addOrderBuilder(parseOrder(parser)); break; default: - checkVersion("Malformed XML: Unknown tag " + tag + " in " + - TAG_RATING_SYSTEM_DEFINITION); + checkVersion( + "Malformed XML: Unknown tag " + + tag + + " in " + + TAG_RATING_SYSTEM_DEFINITION); } break; case XmlPullParser.END_TAG: @@ -227,13 +241,21 @@ public class ContentRatingsParser { builder.setIsCustom(isCustom); return builder.build(); } else { - checkVersion("Malformed XML: Tag mismatch for " + - TAG_RATING_SYSTEM_DEFINITION); + checkVersion( + "Malformed XML: Tag mismatch for " + TAG_RATING_SYSTEM_DEFINITION); } + break; + default: + checkVersion( + "Malformed XML: Unknown event type " + + eventType + + " in " + + TAG_RATING_SYSTEM_DEFINITION); } } - throw new XmlPullParserException(TAG_RATING_SYSTEM_DEFINITION + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_RATING_SYSTEM_DEFINITION + + " section is incomplete or section ending tag is missing"); } private Rating.Builder parseRatingDefinition(XmlResourceParser parser) @@ -265,14 +287,19 @@ public class ContentRatingsParser { } if (contentAgeHint < 0) { - throw new XmlPullParserException("Malformed XML: " + ATTR_CONTENT_AGE_HINT + - " should be a non-negative number"); + throw new XmlPullParserException( + "Malformed XML: " + + ATTR_CONTENT_AGE_HINT + + " should be a non-negative number"); } builder.setContentAgeHint(contentAgeHint); break; default: - checkVersion("Malformed XML: Unknown attribute " + attr + " in " + - TAG_RATING_DEFINITION); + checkVersion( + "Malformed XML: Unknown attribute " + + attr + + " in " + + TAG_RATING_DEFINITION); } } @@ -282,8 +309,11 @@ public class ContentRatingsParser { if (TAG_SUB_RATING.equals(parser.getName())) { builder = parseSubRating(parser, builder); } else { - checkVersion(("Malformed XML: Only " + TAG_SUB_RATING + " is allowed in " + - TAG_RATING_DEFINITION)); + checkVersion( + ("Malformed XML: Only " + + TAG_SUB_RATING + + " is allowed in " + + TAG_RATING_DEFINITION)); } break; case XmlPullParser.END_TAG: @@ -294,8 +324,8 @@ public class ContentRatingsParser { } } } - throw new XmlPullParserException(TAG_RATING_DEFINITION + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_RATING_DEFINITION + " section is incomplete or section ending tag is missing"); } private SubRating.Builder parseSubRatingDefinition(XmlResourceParser parser) @@ -320,8 +350,11 @@ public class ContentRatingsParser { mResources.getDrawable(parser.getAttributeResourceValue(i, 0), null)); break; default: - checkVersion("Malformed XML: Unknown attribute " + attr + " in " + - TAG_SUB_RATING_DEFINITION); + checkVersion( + "Malformed XML: Unknown attribute " + + attr + + " in " + + TAG_SUB_RATING_DEFINITION); } } @@ -331,23 +364,26 @@ public class ContentRatingsParser { if (TAG_SUB_RATING_DEFINITION.equals(parser.getName())) { return builder; } else { - checkVersion("Malformed XML: " + TAG_SUB_RATING_DEFINITION + - " isn't closed"); + checkVersion( + "Malformed XML: " + TAG_SUB_RATING_DEFINITION + " isn't closed"); } break; default: checkVersion("Malformed XML: " + TAG_SUB_RATING_DEFINITION + " has child"); } } - throw new XmlPullParserException(TAG_SUB_RATING_DEFINITION + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_SUB_RATING_DEFINITION + + " section is incomplete or section ending tag is missing"); } private Order.Builder parseOrder(XmlResourceParser parser) throws XmlPullParserException, IOException { Order.Builder builder = new Order.Builder(); - assertEquals(parser.getAttributeCount(), 0, + assertEquals( + parser.getAttributeCount(), + 0, "Malformed XML: Attribute isn't allowed in " + TAG_RATING_ORDER); while (parser.next() != XmlPullParser.END_DOCUMENT) { @@ -355,19 +391,24 @@ public class ContentRatingsParser { case XmlPullParser.START_TAG: if (TAG_RATING.equals(parser.getName())) { builder = parseRating(parser, builder); - } else { - checkVersion("Malformed XML: Only " + TAG_RATING + " is allowed in " + - TAG_RATING_ORDER); + } else { + checkVersion( + "Malformed XML: Only " + + TAG_RATING + + " is allowed in " + + TAG_RATING_ORDER); } break; case XmlPullParser.END_TAG: - assertEquals(parser.getName(), TAG_RATING_ORDER, + assertEquals( + parser.getName(), + TAG_RATING_ORDER, "Malformed XML: Tag mismatch for " + TAG_RATING_ORDER); return builder; } } - throw new XmlPullParserException(TAG_RATING_ORDER + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_RATING_ORDER + " section is incomplete or section ending tag is missing"); } private Order.Builder parseRating(XmlResourceParser parser, Order.Builder builder) @@ -379,8 +420,11 @@ public class ContentRatingsParser { builder.addRatingName(parser.getAttributeValue(i)); break; default: - checkVersion("Malformed XML: " + TAG_RATING_ORDER + " should only contain " - + ATTR_NAME); + checkVersion( + "Malformed XML: " + + TAG_RATING_ORDER + + " should only contain " + + ATTR_NAME); } } @@ -393,8 +437,8 @@ public class ContentRatingsParser { } } } - throw new XmlPullParserException(TAG_RATING + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_RATING + " section is incomplete or section ending tag is missing"); } private Rating.Builder parseSubRating(XmlResourceParser parser, Rating.Builder builder) @@ -406,8 +450,11 @@ public class ContentRatingsParser { builder.addSubRatingName(parser.getAttributeValue(i)); break; default: - checkVersion("Malformed XML: " + TAG_SUB_RATING + " should only contain " + - ATTR_NAME); + checkVersion( + "Malformed XML: " + + TAG_SUB_RATING + + " should only contain " + + ATTR_NAME); } } @@ -420,8 +467,8 @@ public class ContentRatingsParser { } } } - throw new XmlPullParserException(TAG_SUB_RATING + - " section is incomplete or section ending tag is missing"); + throw new XmlPullParserException( + TAG_SUB_RATING + " section is incomplete or section ending tag is missing"); } // Title might be a resource id or a string value. Try loading as an id first, then use the diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java index 2471c565..db1f0a4d 100644 --- a/src/com/android/tv/parental/ParentalControlSettings.java +++ b/src/com/android/tv/parental/ParentalControlSettings.java @@ -19,30 +19,22 @@ package com.android.tv.parental; import android.content.Context; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; - -import com.android.tv.experiments.Experiments; +import com.android.tv.common.experiments.Experiments; import com.android.tv.parental.ContentRatingSystem.Rating; import com.android.tv.parental.ContentRatingSystem.SubRating; import com.android.tv.util.TvSettings; import com.android.tv.util.TvSettings.ContentRatingLevel; - import java.util.HashSet; import java.util.Set; public class ParentalControlSettings { - /** - * The rating and all of its sub-ratings are blocked. - */ + /** The rating and all of its sub-ratings are blocked. */ public static final int RATING_BLOCKED = 0; - /** - * The rating is blocked but not all of its sub-ratings are blocked. - */ + /** The rating is blocked but not all of its sub-ratings are blocked. */ public static final int RATING_BLOCKED_PARTIAL = 1; - /** - * The rating is not blocked. - */ + /** The rating is not blocked. */ public static final int RATING_NOT_BLOCKED = 2; private final Context mContext; @@ -65,8 +57,10 @@ public class ParentalControlSettings { mTvInputManager.setParentalControlsEnabled(enabled); } - public void setContentRatingSystemEnabled(ContentRatingsManager manager, - ContentRatingSystem contentRatingSystem, boolean enabled) { + public void setContentRatingSystemEnabled( + ContentRatingsManager manager, + ContentRatingSystem contentRatingSystem, + boolean enabled) { if (enabled) { TvSettings.addContentRatingSystem(mContext, contentRatingSystem.getId()); @@ -118,8 +112,8 @@ public class ParentalControlSettings { } } - public void setContentRatingLevel(ContentRatingsManager manager, - @ContentRatingLevel int level) { + public void setContentRatingLevel( + ContentRatingsManager manager, @ContentRatingLevel int level) { @ContentRatingLevel int currentLevel = getContentRatingLevel(); if (level == currentLevel) { return; @@ -167,18 +161,17 @@ public class ParentalControlSettings { /** * Sets the blocked status of a given content rating. - * <p> - * Note that a call to this method automatically changes the current rating level to - * {@code TvSettings.CONTENT_RATING_LEVEL_CUSTOM} if needed. - * </p> + * + * <p>Note that a call to this method automatically changes the current rating level to {@code + * TvSettings.CONTENT_RATING_LEVEL_CUSTOM} if needed. * * @param contentRatingSystem The content rating system where the given rating belongs. * @param rating The content rating to set. * @return {@code true} if changed, {@code false} otherwise. * @see #setSubRatingBlocked */ - public boolean setRatingBlocked(ContentRatingSystem contentRatingSystem, Rating rating, - boolean blocked) { + public boolean setRatingBlocked( + ContentRatingSystem contentRatingSystem, Rating rating, boolean blocked) { return setRatingBlockedInternal(contentRatingSystem, rating, null, blocked); } @@ -225,10 +218,9 @@ public class ParentalControlSettings { /** * Sets the blocked status of a given content sub-rating. - * <p> - * Note that a call to this method automatically changes the current rating level to - * {@code TvSettings.CONTENT_RATING_LEVEL_CUSTOM} if needed. - * </p> + * + * <p>Note that a call to this method automatically changes the current rating level to {@code + * TvSettings.CONTENT_RATING_LEVEL_CUSTOM} if needed. * * @param contentRatingSystem The content rating system where the given rating belongs. * @param rating The content rating associated with the given sub-rating. @@ -236,8 +228,11 @@ public class ParentalControlSettings { * @return {@code true} if changed, {@code false} otherwise. * @see #setRatingBlocked */ - public boolean setSubRatingBlocked(ContentRatingSystem contentRatingSystem, Rating rating, - SubRating subRating, boolean blocked) { + public boolean setSubRatingBlocked( + ContentRatingSystem contentRatingSystem, + Rating rating, + SubRating subRating, + boolean blocked) { return setRatingBlockedInternal(contentRatingSystem, rating, subRating, blocked); } @@ -249,16 +244,20 @@ public class ParentalControlSettings { * @param subRating The content sub-rating to check. * @return {@code true} if blocked, {@code false} otherwise. */ - public boolean isSubRatingEnabled(ContentRatingSystem contentRatingSystem, Rating rating, - SubRating subRating) { + public boolean isSubRatingEnabled( + ContentRatingSystem contentRatingSystem, Rating rating, SubRating subRating) { return mRatings.contains(toTvContentRating(contentRatingSystem, rating, subRating)); } - private boolean setRatingBlockedInternal(ContentRatingSystem contentRatingSystem, Rating rating, - SubRating subRating, boolean blocked) { - TvContentRating tvContentRating = (subRating == null) - ? toTvContentRating(contentRatingSystem, rating) - : toTvContentRating(contentRatingSystem, rating, subRating); + private boolean setRatingBlockedInternal( + ContentRatingSystem contentRatingSystem, + Rating rating, + SubRating subRating, + boolean blocked) { + TvContentRating tvContentRating = + (subRating == null) + ? toTvContentRating(contentRatingSystem, rating) + : toTvContentRating(contentRatingSystem, rating, subRating); boolean changed; if (blocked) { changed = mRatings.add(tvContentRating); @@ -280,8 +279,8 @@ public class ParentalControlSettings { } /** - * Returns the blocked status of a given rating. The status can be one of the followings: - * {@link #RATING_BLOCKED}, {@link #RATING_BLOCKED_PARTIAL} and {@link #RATING_NOT_BLOCKED} + * Returns the blocked status of a given rating. The status can be one of the followings: {@link + * #RATING_BLOCKED}, {@link #RATING_BLOCKED_PARTIAL} and {@link #RATING_NOT_BLOCKED} */ public int getBlockedStatus(ContentRatingSystem contentRatingSystem, Rating rating) { if (isRatingBlocked(contentRatingSystem, rating)) { @@ -295,15 +294,18 @@ public class ParentalControlSettings { return RATING_NOT_BLOCKED; } - private TvContentRating toTvContentRating(ContentRatingSystem contentRatingSystem, - Rating rating) { - return TvContentRating.createRating(contentRatingSystem.getDomain(), - contentRatingSystem.getName(), rating.getName()); + private TvContentRating toTvContentRating( + ContentRatingSystem contentRatingSystem, Rating rating) { + return TvContentRating.createRating( + contentRatingSystem.getDomain(), contentRatingSystem.getName(), rating.getName()); } - private TvContentRating toTvContentRating(ContentRatingSystem contentRatingSystem, - Rating rating, SubRating subRating) { - return TvContentRating.createRating(contentRatingSystem.getDomain(), - contentRatingSystem.getName(), rating.getName(), subRating.getName()); + private TvContentRating toTvContentRating( + ContentRatingSystem contentRatingSystem, Rating rating, SubRating subRating) { + return TvContentRating.createRating( + contentRatingSystem.getDomain(), + contentRatingSystem.getName(), + rating.getName(), + subRating.getName()); } } diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java index 6026897b..54745f3b 100644 --- a/src/com/android/tv/perf/EventNames.java +++ b/src/com/android/tv/perf/EventNames.java @@ -16,12 +16,11 @@ package com.android.tv.perf; -import android.support.annotation.StringDef; +import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.support.annotation.StringDef; import java.lang.annotation.Retention; -import static java.lang.annotation.RetentionPolicy.SOURCE; - /** * Constants for performance event names. * @@ -47,8 +46,8 @@ public final class EventNames { public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart"; public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume"; /** - * Event name for query running time of on-device search in - * {@link com.android.tv.search.LocalSearchProvider}. + * Event name for query running time of on-device search in {@link + * com.android.tv.search.LocalSearchProvider}. */ public static final String ON_DEVICE_SEARCH = "OnDeviceSearch"; diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java index 40368b41..111aa851 100644 --- a/src/com/android/tv/perf/PerformanceMonitor.java +++ b/src/com/android/tv/perf/PerformanceMonitor.java @@ -16,10 +16,10 @@ package com.android.tv.perf; -import android.content.Context; - import static com.android.tv.perf.EventNames.EventName; +import android.content.Context; + /** Measures Performance. */ public interface PerformanceMonitor { @@ -62,7 +62,6 @@ public interface PerformanceMonitor { */ TimerEvent startTimer(); - /** * Stops timer for a specific event and records the timer duration. passing a null TimerEvent * will cause this operation to be skipped. diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/AbstractGlobalKeyReceiver.java index cc8e76c4..f88bd8a8 100644 --- a/src/com/android/tv/receiver/GlobalKeyReceiver.java +++ b/src/com/android/tv/receiver/AbstractGlobalKeyReceiver.java @@ -23,15 +23,14 @@ import android.os.AsyncTask; import android.provider.Settings; import android.util.Log; import android.view.KeyEvent; - +import com.android.tv.Starter; import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; -/** - * Handles global keys. - */ -public class GlobalKeyReceiver extends BroadcastReceiver { +/** Handles global keys. */ +public abstract class AbstractGlobalKeyReceiver extends BroadcastReceiver { private static final boolean DEBUG = false; - private static final String TAG = "GlobalKeyReceiver"; + private static final String TAG = "AbstractGlobalKeyReceiver"; private static final String ACTION_GLOBAL_BUTTON = "android.intent.action.GLOBAL_BUTTON"; // Settings.Secure.USER_SETUP_COMPLETE is hidden. @@ -42,11 +41,11 @@ public class GlobalKeyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { + if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); return; } - TvApplication.setCurrentRunningProcess(context, true); + Starter.start(context); Context appContext = context.getApplicationContext(); if (DEBUG) Log.d(TAG, "onReceive: " + intent); if (sUserSetupComplete) { @@ -55,8 +54,11 @@ public class GlobalKeyReceiver extends BroadcastReceiver { new AsyncTask<Void, Void, Boolean>() { @Override protected Boolean doInBackground(Void... params) { - return Settings.Secure.getInt(appContext.getContentResolver(), - SETTINGS_USER_SETUP_COMPLETE, 0) != 0; + return Settings.Secure.getInt( + appContext.getContentResolver(), + SETTINGS_USER_SETUP_COMPLETE, + 0) + != 0; } @Override @@ -82,6 +84,9 @@ public class GlobalKeyReceiver extends BroadcastReceiver { // Workaround for b/23947504, the same key event may be sent twice, filter it. sLastEventTime = eventTime; switch (keyCode) { + case KeyEvent.KEYCODE_DVR: + ((TvApplication) appContext).handleDvrKey(); + break; case KeyEvent.KEYCODE_GUIDE: ((TvApplication) appContext).handleGuideKey(); break; diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java index 313b2dfa..3fb66245 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -24,17 +24,15 @@ import android.media.AudioFormat; import android.media.AudioManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; -import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.util.SharedPreferencesUtils; /** * Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities to Google - * Analytics and listeners. Call {@link #register} to start receiving notifications, and - * {@link #unregister} to stop. + * Analytics and listeners. Call {@link #register} to start receiving notifications, and {@link + * #unregister} to stop. */ public final class AudioCapabilitiesReceiver { private static final String SETTINGS_KEY_AC3_PASSTHRU_REPORTED = "ac3_passthrough_reported"; @@ -50,8 +48,7 @@ public final class AudioCapabilitiesReceiver { private final Context mContext; private final Analytics mAnalytics; private final Tracker mTracker; - @Nullable - private final OnAc3PassthroughCapabilityChangeListener mListener; + @Nullable private final OnAc3PassthroughCapabilityChangeListener mListener; private final BroadcastReceiver mReceiver = new HdmiAudioPlugBroadcastReceiver(); /** @@ -60,12 +57,12 @@ public final class AudioCapabilitiesReceiver { * @param context context for registering to receive broadcasts * @param listener listener which receives AC3 passthrough capability change notification */ - public AudioCapabilitiesReceiver(@NonNull Context context, - @Nullable OnAc3PassthroughCapabilityChangeListener listener) { + public AudioCapabilitiesReceiver( + @NonNull Context context, @Nullable OnAc3PassthroughCapabilityChangeListener listener) { mContext = context; - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mAnalytics = appSingletons.getAnalytics(); - mTracker = appSingletons.getTracker(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mAnalytics = tvSingletons.getAnalytics(); + mTracker = tvSingletons.getTracker(); mListener = listener; } @@ -121,8 +118,8 @@ public final class AudioCapabilitiesReceiver { } private SharedPreferences getSharedPreferences() { - return mContext.getSharedPreferences(SharedPreferencesUtils.SHARED_PREF_AUDIO_CAPABILITIES, - Context.MODE_PRIVATE); + return mContext.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_AUDIO_CAPABILITIES, Context.MODE_PRIVATE); } private boolean getBoolean(String key, boolean def) { @@ -141,13 +138,9 @@ public final class AudioCapabilitiesReceiver { getSharedPreferences().edit().putInt(key, val).apply(); } - /** - * Listener notified when AC3 passthrough capability changes. - */ + /** Listener notified when AC3 passthrough capability changes. */ public interface OnAc3PassthroughCapabilityChangeListener { - /** - * Called when the AC3 passthrough capability changes. - */ + /** Called when the AC3 passthrough capability changes. */ void onAc3PassthroughCapabilityChange(boolean capability); } } diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 369e7d54..d8528bb5 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -23,10 +23,10 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Build; import android.util.Log; - -import com.android.tv.Features; +import com.android.tv.Starter; import com.android.tv.TvActivity; -import com.android.tv.TvApplication; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.dvr.recorder.DvrRecordingService; import com.android.tv.dvr.recorder.RecordingScheduler; import com.android.tv.recommendation.ChannelPreviewUpdater; @@ -38,11 +38,12 @@ import com.android.tv.util.SetupUtils; * Boot completed receiver for TV app. * * <p>It's used to + * * <ul> - * <li>start the {@link NotificationService} for recommendation</li> - * <li>grant permission to the TIS's </li> - * <li>enable {@link TvActivity} if necessary</li> - * <li>start the {@link DvrRecordingService} </li> + * <li>start the {@link NotificationService} for recommendation + * <li>grant permission to the TIS's + * <li>enable {@link TvActivity} if necessary + * <li>start the {@link DvrRecordingService} * </ul> */ public class BootCompletedReceiver extends BroadcastReceiver { @@ -51,12 +52,12 @@ public class BootCompletedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { + if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); return; } if (DEBUG) Log.d(TAG, "boot completed " + intent); - TvApplication.setCurrentRunningProcess(context, true); + Starter.start(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ChannelPreviewUpdater.getInstance(context).updatePreviewDataForChannelsImmediately(); @@ -69,7 +70,7 @@ public class BootCompletedReceiver extends BroadcastReceiver { // Grant permission to already set up packages after the system has finished booting. SetupUtils.grantEpgPermissionToSetUpPackages(context); - if (Features.UNHIDE.isEnabled(context)) { + if (TvFeatures.UNHIDE.isEnabled(context)) { if (OnboardingUtils.isFirstBoot(context)) { // Enable the application if this is the first "unhide" feature is enabled just in // case when the app has been disabled before. @@ -77,14 +78,14 @@ public class BootCompletedReceiver extends BroadcastReceiver { ComponentName name = new ComponentName(context, TvActivity.class); if (pm.getComponentEnabledSetting(name) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - pm.setComponentEnabledSetting(name, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); + pm.setComponentEnabledSetting( + name, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); } OnboardingUtils.setFirstBootCompleted(context); } } - RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler(); + RecordingScheduler scheduler = TvSingletons.getSingletons(context).getRecordingScheduler(); if (scheduler != null) { scheduler.updateAndStartServiceIfNeeded(); } diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index 124172f0..07f5d6be 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -21,24 +21,24 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.Log; - -import com.android.tv.TvApplication; +import com.android.tv.Starter; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.util.Partner; +import com.google.android.tv.partner.support.EpgContract; -/** - * A class for handling the broadcast intents from PackageManager. - */ +/** A class for handling the broadcast intents from PackageManager. */ public class PackageIntentsReceiver extends BroadcastReceiver { private static final String TAG = "PackageIntentsReceiver"; @Override public void onReceive(Context context, Intent intent) { - if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { + if (!TvSingletons.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); return; } - TvApplication.setCurrentRunningProcess(context, true); - ((TvApplication) context.getApplicationContext()).handleInputCountChanged(); + Starter.start(context); + ((TvSingletons) context.getApplicationContext()).handleInputCountChanged(); Uri uri = intent.getData(); final String packageName = (uri != null ? uri.getSchemeSpecificPart() : null); diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java index 2709ebe1..410b8252 100644 --- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java +++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java @@ -28,16 +28,14 @@ import android.support.annotation.RequiresApi; import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; import com.android.tv.data.PreviewDataManager; import com.android.tv.data.PreviewProgramContent; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -48,23 +46,19 @@ import java.util.concurrent.TimeUnit; @RequiresApi(Build.VERSION_CODES.O) public class ChannelPreviewUpdater { private static final String TAG = "ChannelPreviewUpdater"; - // STOPSHIP: set it to false. - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001; private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10); // The left time of a program should meet the threshold so that it could be recommended. - private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = - TimeUnit.MINUTES.toMillis(10); - private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% + private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = TimeUnit.MINUTES.toMillis(10); + private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% private static final int RECOMMENDATION_COUNT = 6; private static final int MIN_COUNT_TO_ADD_ROW = 4; private static ChannelPreviewUpdater sChannelPreviewUpdater; - /** - * Creates and returns the {@link ChannelPreviewUpdater}. - */ + /** Creates and returns the {@link ChannelPreviewUpdater}. */ public static ChannelPreviewUpdater getInstance(Context context) { if (sChannelPreviewUpdater == null) { sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext()); @@ -82,21 +76,22 @@ public class ChannelPreviewUpdater { private boolean mNeedUpdateAfterRecommenderReady = false; - private Recommender.Listener mRecommenderListener = new Recommender.Listener() { - @Override - public void onRecommenderReady() { - if (mNeedUpdateAfterRecommenderReady) { - if (DEBUG) Log.d(TAG, "Recommender is ready"); - updatePreviewDataForChannelsImmediately(); - mNeedUpdateAfterRecommenderReady = false; - } - } + private Recommender.Listener mRecommenderListener = + new Recommender.Listener() { + @Override + public void onRecommenderReady() { + if (mNeedUpdateAfterRecommenderReady) { + if (DEBUG) Log.d(TAG, "Recommender is ready"); + updatePreviewDataForChannelsImmediately(); + mNeedUpdateAfterRecommenderReady = false; + } + } - @Override - public void onRecommendationChanged() { - updatePreviewDataForChannelsImmediately(); - } - }; + @Override + public void onRecommendationChanged() { + updatePreviewDataForChannelsImmediately(); + } + }; private ChannelPreviewUpdater(Context context) { mContext = context; @@ -104,15 +99,13 @@ public class ChannelPreviewUpdater { mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1); mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5); mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); - ApplicationSingletons appSingleton = TvApplication.getSingletons(context); - mPreviewDataManager = appSingleton.getPreviewDataManager(); - mParentalControlSettings = appSingleton.getTvInputManagerHelper() - .getParentalControlSettings(); + TvSingletons tvSingleton = TvSingletons.getSingletons(context); + mPreviewDataManager = tvSingleton.getPreviewDataManager(); + mParentalControlSettings = + tvSingleton.getTvInputManagerHelper().getParentalControlSettings(); } - /** - * Starts the routine service for updating the preview programs. - */ + /** Starts the routine service for updating the preview programs. */ public void startRoutineService() { JobScheduler jobScheduler = (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); @@ -120,11 +113,13 @@ public class ChannelPreviewUpdater { if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists"); return; } - JobInfo job = new JobInfo.Builder(UPATE_PREVIEW_PROGRAMS_JOB_ID, - new ComponentName(mContext, ChannelPreviewUpdateService.class)) - .setPeriodic(ROUTINE_INTERVAL_MS) - .setPersisted(true) - .build(); + JobInfo job = + new JobInfo.Builder( + UPATE_PREVIEW_PROGRAMS_JOB_ID, + new ComponentName(mContext, ChannelPreviewUpdateService.class)) + .setPeriodic(ROUTINE_INTERVAL_MS) + .setPersisted(true) + .build(); if (jobScheduler.schedule(job) < 0) { Log.i(TAG, "JobScheduler failed to schedule the job"); } @@ -138,9 +133,7 @@ public class ChannelPreviewUpdater { updatePreviewDataForChannelsImmediately(); } - /** - * Updates the preview programs table. - */ + /** Updates the preview programs table. */ public void updatePreviewDataForChannelsImmediately() { if (!mRecommender.isReady()) { mNeedUpdateAfterRecommenderReady = true; @@ -148,16 +141,17 @@ public class ChannelPreviewUpdater { } if (!mPreviewDataManager.isLoadFinished()) { - mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() { - @Override - public void onPreviewDataLoadFinished() { - mPreviewDataManager.removeListener(this); - updatePreviewDataForChannels(); - } + mPreviewDataManager.addListener( + new PreviewDataManager.PreviewDataListener() { + @Override + public void onPreviewDataLoadFinished() { + mPreviewDataManager.removeListener(this); + updatePreviewDataForChannels(); + } - @Override - public void onPreviewDataUpdateFinished() { } - }); + @Override + public void onPreviewDataUpdateFinished() {} + }); return; } updatePreviewDataForChannels(); @@ -225,8 +219,9 @@ public class ChannelPreviewUpdater { } private void updatePreviewDataForChannelsInternal(Set<Program> programs) { - long defaultPreviewChannelId = mPreviewDataManager.getPreviewChannelId( - PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL); + long defaultPreviewChannelId = + mPreviewDataManager.getPreviewChannelId( + PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL); if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) { // Only create if there is enough programs if (programs.size() > MIN_COUNT_TO_ADD_ROW) { @@ -248,7 +243,8 @@ public class ChannelPreviewUpdater { }); } } else { - updatePreviewProgramsForPreviewChannel(defaultPreviewChannelId, + updatePreviewProgramsForPreviewChannel( + defaultPreviewChannelId, generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs)); } } @@ -266,39 +262,38 @@ public class ChannelPreviewUpdater { return result; } - private void updatePreviewProgramsForPreviewChannel(long previewChannelId, - Set<PreviewProgramContent> previewProgramContents) { - PreviewDataManager.PreviewDataListener previewDataListener - = new PreviewDataManager.PreviewDataListener() { - @Override - public void onPreviewDataLoadFinished() { } - - @Override - public void onPreviewDataUpdateFinished() { - mPreviewDataManager.removeListener(this); - if (mJobService != null && mJobParams != null) { - if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService"); - mJobService.jobFinished(mJobParams, false); - mJobService = null; - mJobParams = null; - } else { - if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService"); - } - } - }; + private void updatePreviewProgramsForPreviewChannel( + long previewChannelId, Set<PreviewProgramContent> previewProgramContents) { + PreviewDataManager.PreviewDataListener previewDataListener = + new PreviewDataManager.PreviewDataListener() { + @Override + public void onPreviewDataLoadFinished() {} + + @Override + public void onPreviewDataUpdateFinished() { + mPreviewDataManager.removeListener(this); + if (mJobService != null && mJobParams != null) { + if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService"); + mJobService.jobFinished(mJobParams, false); + mJobService = null; + mJobParams = null; + } else { + if (DEBUG) + Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService"); + } + } + }; mPreviewDataManager.updatePreviewProgramsForChannel( previewChannelId, previewProgramContents, previewDataListener); } - /** - * Job to execute the update of preview programs. - */ + /** Job to execute the update of preview programs. */ public static class ChannelPreviewUpdateService extends JobService { private ChannelPreviewUpdater mChannelPreviewUpdater; @Override public void onCreate() { - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate"); mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this); } diff --git a/src/com/android/tv/recommendation/ChannelRecord.java b/src/com/android/tv/recommendation/ChannelRecord.java index 26f0fbf0..c7a7cb37 100644 --- a/src/com/android/tv/recommendation/ChannelRecord.java +++ b/src/com/android/tv/recommendation/ChannelRecord.java @@ -17,14 +17,12 @@ package com.android.tv.recommendation; import android.content.Context; +import android.support.annotation.GuardedBy; import android.support.annotation.VisibleForTesting; - -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; -import com.android.tv.util.Utils; - +import com.android.tv.data.api.Channel; import java.util.ArrayDeque; import java.util.Deque; @@ -32,7 +30,10 @@ public class ChannelRecord { // TODO: decide the value for max history size. @VisibleForTesting static final int MAX_HISTORY_SIZE = 100; private final Context mContext; + + @GuardedBy("this") private final Deque<WatchedProgram> mWatchHistory; + private Program mCurrentProgram; private Channel mChannel; private long mTotalWatchDurationMs; @@ -62,7 +63,7 @@ public class ChannelRecord { mInputRemoved = removed; } - public long getLastWatchEndTimeMs() { + public synchronized long getLastWatchEndTimeMs() { WatchedProgram p = mWatchHistory.peekLast(); return (p == null) ? 0 : p.getWatchEndTimeMs(); } @@ -71,7 +72,7 @@ public class ChannelRecord { long time = System.currentTimeMillis(); if (mCurrentProgram == null || mCurrentProgram.getEndTimeUtcMillis() < time) { ProgramDataManager manager = - TvApplication.getSingletons(mContext).getProgramDataManager(); + TvSingletons.getSingletons(mContext).getProgramDataManager(); mCurrentProgram = manager.getCurrentProgram(mChannel.getId()); } return mCurrentProgram; @@ -81,11 +82,11 @@ public class ChannelRecord { return mTotalWatchDurationMs; } - public final WatchedProgram[] getWatchHistory() { + public final synchronized WatchedProgram[] getWatchHistory() { return mWatchHistory.toArray(new WatchedProgram[mWatchHistory.size()]); } - public void logWatchHistory(WatchedProgram p) { + public synchronized void logWatchHistory(WatchedProgram p) { mWatchHistory.offer(p); mTotalWatchDurationMs += p.getWatchedDurationMs(); if (mWatchHistory.size() > MAX_HISTORY_SIZE) { diff --git a/src/com/android/tv/recommendation/FavoriteChannelEvaluator.java b/src/com/android/tv/recommendation/FavoriteChannelEvaluator.java index 9a6de7e2..8b0a3502 100644 --- a/src/com/android/tv/recommendation/FavoriteChannelEvaluator.java +++ b/src/com/android/tv/recommendation/FavoriteChannelEvaluator.java @@ -19,7 +19,7 @@ package com.android.tv.recommendation; import java.util.List; public class FavoriteChannelEvaluator extends Recommender.Evaluator { - private static final long MIN_WATCH_PERIOD_MS = 1000 * 60 * 60 * 24; // 1 day + private static final long MIN_WATCH_PERIOD_MS = 1000 * 60 * 60 * 24; // 1 day // When there is no watch history, use the current time as a default value. private long mEarliestWatchStartTimeMs = System.currentTimeMillis(); @@ -46,7 +46,6 @@ public class FavoriteChannelEvaluator extends Recommender.Evaluator { } long watchPeriodMs = System.currentTimeMillis() - mEarliestWatchStartTimeMs; - return (double) cr.getTotalWatchDurationMs() / - Math.max(watchPeriodMs, MIN_WATCH_PERIOD_MS); + return (double) cr.getTotalWatchDurationMs() / Math.max(watchPeriodMs, MIN_WATCH_PERIOD_MS); } } diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index a44eca41..f40a0862 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -40,42 +40,39 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseLongArray; import android.view.View; - -import com.android.tv.ApplicationSingletons; import com.android.tv.MainActivityWrapper.OnCurrentChannelChangeListener; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.Starter; +import com.android.tv.TvSingletons; +import com.android.tv.common.CommonConstants; import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.util.BitmapUtils; -import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; -import com.android.tv.util.ImageLoader; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - +import com.android.tv.util.images.BitmapUtils; +import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; +import com.android.tv.util.images.ImageLoader; import java.util.ArrayList; import java.util.List; -/** - * A local service for notify recommendation at home launcher. - */ -public class NotificationService extends Service implements Recommender.Listener, - OnCurrentChannelChangeListener { +/** A local service for notify recommendation at home launcher. */ +public class NotificationService extends Service + implements Recommender.Listener, OnCurrentChannelChangeListener { private static final String TAG = "NotificationService"; private static final boolean DEBUG = false; public static final String ACTION_SHOW_RECOMMENDATION = - "com.android.tv.notification.ACTION_SHOW_RECOMMENDATION"; + CommonConstants.BASE_PACKAGE + ".notification.ACTION_SHOW_RECOMMENDATION"; public static final String ACTION_HIDE_RECOMMENDATION = - "com.android.tv.notification.ACTION_HIDE_RECOMMENDATION"; + CommonConstants.BASE_PACKAGE + ".notification.ACTION_HIDE_RECOMMENDATION"; /** - * Recommendation intent has an extra data for the recommendation type. It'll be also - * sent to a TV input as a tune parameter. + * Recommendation intent has an extra data for the recommendation type. It'll be also sent to a + * TV input as a tune parameter. */ public static final String TUNE_PARAMS_RECOMMENDATION_TYPE = - "com.android.tv.recommendation_type"; + CommonConstants.BASE_PACKAGE + ".recommendation_type"; private static final String TYPE_RANDOM_RECOMMENDATION = "random"; private static final String TYPE_ROUTINE_WATCH_RECOMMENDATION = "routine_watch"; @@ -92,9 +89,9 @@ public class NotificationService extends Service implements Recommender.Listener private static final int MSG_UPDATE_RECOMMENDATION = 1002; private static final int MSG_HIDE_RECOMMENDATION = 1003; - private static final long RECOMMENDATION_RETRY_TIME_MS = 5 * 60 * 1000; // 5 min - private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = 10 * 60 * 1000; // 10 min - private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% + private static final long RECOMMENDATION_RETRY_TIME_MS = 5 * 60 * 1000; // 5 min + private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = 10 * 60 * 1000; // 10 min + private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% private static final int MAX_PROGRAM_UPDATE_COUNT = 20; private TvInputManagerHelper mTvInputManagerHelper; @@ -126,17 +123,17 @@ public class NotificationService extends Service implements Recommender.Listener @Override public void onCreate() { if (DEBUG) Log.d(TAG, "onCreate"); - TvApplication.setCurrentRunningProcess(this, true); + Starter.start(this); super.onCreate(); mCurrentNotificationCount = 0; mNotificationChannels = new long[NOTIFICATION_COUNT]; for (int i = 0; i < NOTIFICATION_COUNT; ++i) { mNotificationChannels[i] = Channel.INVALID_ID; } - mNotificationCardMaxWidth = getResources().getDimensionPixelSize( - R.dimen.notif_card_img_max_width); - mNotificationCardHeight = getResources().getDimensionPixelSize( - R.dimen.notif_card_img_height); + mNotificationCardMaxWidth = + getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNotificationCardHeight = + getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); mCardImageMaxWidth = getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); mCardImageMinWidth = getResources().getDimensionPixelSize(R.dimen.notif_card_img_min_width); @@ -150,17 +147,17 @@ public class NotificationService extends Service implements Recommender.Listener getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - ApplicationSingletons appSingletons = TvApplication.getSingletons(this); - mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); + TvSingletons tvSingletons = TvSingletons.getSingletons(this); + mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper(); mHandlerThread = new HandlerThread("tv notification"); mHandlerThread.start(); mHandler = new NotificationHandler(mHandlerThread.getLooper(), this); mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER); // Just called for early initialization. - appSingletons.getChannelDataManager(); - appSingletons.getProgramDataManager(); - appSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this); + tvSingletons.getChannelDataManager(); + tvSingletons.getProgramDataManager(); + tvSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this); } @UiThread @@ -178,8 +175,8 @@ public class NotificationService extends Service implements Recommender.Listener mRecommender.registerEvaluator(new RandomEvaluator()); } else if (TYPE_ROUTINE_WATCH_RECOMMENDATION.equals(mRecommendationType)) { mRecommender.registerEvaluator(new RoutineWatchEvaluator()); - } else if (TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION - .equals(mRecommendationType)) { + } else if (TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION.equals( + mRecommendationType)) { mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5); mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); } else { @@ -189,6 +186,9 @@ public class NotificationService extends Service implements Recommender.Listener } private void handleShowRecommendation() { + if (mRecommender == null) { + return; + } if (!mRecommender.isReady()) { mShowRecommendationAfterRecommenderReady = true; } else { @@ -197,13 +197,16 @@ public class NotificationService extends Service implements Recommender.Listener } private void handleUpdateRecommendation(int notificationId, Channel channel) { - if (mNotificationChannels[notificationId] == Channel.INVALID_ID || !sendNotification( - channel.getId(), notificationId)) { + if (mNotificationChannels[notificationId] == Channel.INVALID_ID + || !sendNotification(channel.getId(), notificationId)) { changeRecommendation(notificationId); } } private void handleHideRecommendation() { + if (mRecommender == null) { + return; + } if (!mRecommender.isReady()) { mShowRecommendationAfterRecommenderReady = false; } else { @@ -213,7 +216,8 @@ public class NotificationService extends Service implements Recommender.Listener @Override public void onDestroy() { - TvApplication.getSingletons(this).getMainActivityWrapper() + TvSingletons.getSingletons(this) + .getMainActivityWrapper() .removeOnCurrentChannelChangeListener(this); if (mRecommender != null) { mRecommender.release(); @@ -316,7 +320,7 @@ public class NotificationService extends Service implements Recommender.Listener } for (Channel c : channels) { if (!isNotifiedChannel(c.getId())) { - if(sendNotification(c.getId(), notificationId)) { + if (sendNotification(c.getId(), notificationId)) { return; } } @@ -334,13 +338,13 @@ public class NotificationService extends Service implements Recommender.Listener } private void hideAllRecommendation() { - for (int i = 0; i < NOTIFICATION_COUNT; ++i) { - if (mNotificationChannels[i] != Channel.INVALID_ID) { - mNotificationChannels[i] = Channel.INVALID_ID; - mNotificationManager.cancel(NOTIFY_TAG, i); - } - } - mCurrentNotificationCount = 0; + for (int i = 0; i < NOTIFICATION_COUNT; ++i) { + if (mNotificationChannels[i] != Channel.INVALID_ID) { + mNotificationChannels[i] = Channel.INVALID_ID; + mNotificationManager.cancel(NOTIFY_TAG, i); + } + } + mCurrentNotificationCount = 0; } private boolean sendNotification(final long channelId, final int notificationId) { @@ -350,8 +354,13 @@ public class NotificationService extends Service implements Recommender.Listener } final Channel channel = cr.getChannel(); if (DEBUG) { - Log.d(TAG, "sendNotification (channelName=" + channel.getDisplayName() + " notifyId=" - + notificationId + ")"); + Log.d( + TAG, + "sendNotification (channelName=" + + channel.getDisplayName() + + " notifyId=" + + notificationId + + ")"); } // TODO: Move some checking logic into TvRecommendation. @@ -363,17 +372,18 @@ public class NotificationService extends Service implements Recommender.Listener if (inputInfo == null) { return false; } - final String inputDisplayName = inputInfo.loadLabel(this).toString(); final Program program = Utils.getCurrentProgram(this, channel.getId()); if (program == null) { return false; } - final long programDurationMs = program.getEndTimeUtcMillis() - - program.getStartTimeUtcMillis(); + final long programDurationMs = + program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); - final int programProgress = (programDurationMs <= 0) ? -1 - : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); + final int programProgress = + (programDurationMs <= 0) + ? -1 + : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); // We recommend those programs that meet the condition only. if (programProgress >= RECOMMENDATION_THRESHOLD_PROGRESS @@ -382,19 +392,24 @@ public class NotificationService extends Service implements Recommender.Listener } // We don't trust TIS to provide us with proper sized image - ScaledBitmapInfo posterArtBitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(this, - program.getPosterArtUri(), (int) mNotificationCardMaxWidth, - (int) mNotificationCardHeight); + ScaledBitmapInfo posterArtBitmapInfo = + BitmapUtils.decodeSampledBitmapFromUriString( + this, + program.getPosterArtUri(), + (int) mNotificationCardMaxWidth, + (int) mNotificationCardHeight); if (posterArtBitmapInfo == null) { Log.e(TAG, "Failed to decode poster image for " + program.getPosterArtUri()); return false; } final Bitmap posterArtBitmap = posterArtBitmapInfo.bitmap; - channel.loadBitmap(this, Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, mChannelLogoMaxWidth, + channel.loadBitmap( + this, + Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mChannelLogoMaxWidth, mChannelLogoMaxHeight, - createChannelLogoCallback(this, notificationId, inputDisplayName, channel, program, - posterArtBitmap)); + createChannelLogoCallback(this, notificationId, channel, program, posterArtBitmap)); if (mNotificationChannels[notificationId] == Channel.INVALID_ID) { ++mCurrentNotificationCount; @@ -404,62 +419,77 @@ public class NotificationService extends Service implements Recommender.Listener return true; } - @NonNull - private static ImageLoader.ImageLoaderCallback<NotificationService> createChannelLogoCallback( - NotificationService service, final int notificationId, final String inputDisplayName, - final Channel channel, final Program program, final Bitmap posterArtBitmap) { - return new ImageLoader.ImageLoaderCallback<NotificationService>(service) { - @Override - public void onBitmapLoaded(NotificationService service, Bitmap channelLogo) { - service.sendNotification(notificationId, channelLogo, channel, posterArtBitmap, - program, inputDisplayName); - } - }; - } - - private void sendNotification(int notificationId, Bitmap channelLogo, Channel channel, - Bitmap posterArtBitmap, Program program, String inputDisplayName) { - final long programDurationMs = program.getEndTimeUtcMillis() - program - .getStartTimeUtcMillis(); + private void sendNotification( + int notificationId, + Bitmap channelLogo, + Channel channel, + Bitmap posterArtBitmap, + Program program) { + final long programDurationMs = + program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); - final int programProgress = (programDurationMs <= 0) ? -1 - : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); + final int programProgress = + (programDurationMs <= 0) + ? -1 + : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri()); intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0); // This callback will run on the main thread. - Bitmap largeIconBitmap = (channelLogo == null) ? posterArtBitmap - : overlayChannelLogo(channelLogo, posterArtBitmap); + Bitmap largeIconBitmap = + (channelLogo == null) + ? posterArtBitmap + : overlayChannelLogo(channelLogo, posterArtBitmap); String channelDisplayName = channel.getDisplayName(); - Notification notification = new Notification.Builder(this) - .setContentIntent(notificationIntent) - .setContentTitle(program.getTitle()) - .setContentText(TextUtils.isEmpty(channelDisplayName) ? channel.getDisplayNumber() - : channelDisplayName) - .setContentInfo(channelDisplayName) - .setAutoCancel(true).setLargeIcon(largeIconBitmap) - .setSmallIcon(R.drawable.ic_launcher_s) - .setCategory(Notification.CATEGORY_RECOMMENDATION) - .setProgress((programProgress > 0) ? 100 : 0, programProgress, false) - .setSortKey(mRecommender.getChannelSortKey(channel.getId())) - .build(); + Notification notification = + new Notification.Builder(this) + .setContentIntent(notificationIntent) + .setContentTitle(program.getTitle()) + .setContentText( + TextUtils.isEmpty(channelDisplayName) + ? channel.getDisplayNumber() + : channelDisplayName) + .setContentInfo(channelDisplayName) + .setAutoCancel(true) + .setLargeIcon(largeIconBitmap) + .setSmallIcon(R.drawable.ic_launcher_s) + .setCategory(Notification.CATEGORY_RECOMMENDATION) + .setProgress((programProgress > 0) ? 100 : 0, programProgress, false) + .setSortKey(mRecommender.getChannelSortKey(channel.getId())) + .build(); notification.color = getResources().getColor(R.color.recommendation_card_background, null); if (!TextUtils.isEmpty(program.getThumbnailUri())) { - notification.extras - .putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, program.getThumbnailUri()); + notification.extras.putString( + Notification.EXTRA_BACKGROUND_IMAGE_URI, program.getThumbnailUri()); } mNotificationManager.notify(NOTIFY_TAG, notificationId, notification); Message msg = mHandler.obtainMessage(MSG_UPDATE_RECOMMENDATION, notificationId, 0, channel); mHandler.sendMessageDelayed(msg, programDurationMs / MAX_PROGRAM_UPDATE_COUNT); } + @NonNull + private static ImageLoader.ImageLoaderCallback<NotificationService> createChannelLogoCallback( + NotificationService service, + final int notificationId, + final Channel channel, + final Program program, + final Bitmap posterArtBitmap) { + return new ImageLoader.ImageLoaderCallback<NotificationService>(service) { + @Override + public void onBitmapLoaded(NotificationService service, Bitmap channelLogo) { + service.sendNotification( + notificationId, channelLogo, channel, posterArtBitmap, program); + } + }; + } + private Bitmap overlayChannelLogo(Bitmap logo, Bitmap background) { - Bitmap result = BitmapUtils.getScaledMutableBitmap( - background, Integer.MAX_VALUE, mCardImageHeight); - Bitmap scaledLogo = BitmapUtils.scaleBitmap( - logo, mChannelLogoMaxWidth, mChannelLogoMaxHeight); + Bitmap result = + BitmapUtils.getScaledMutableBitmap(background, Integer.MAX_VALUE, mCardImageHeight); + Bitmap scaledLogo = + BitmapUtils.scaleBitmap(logo, mChannelLogoMaxWidth, mChannelLogoMaxHeight); Canvas canvas; try { canvas = new Canvas(result); @@ -524,27 +554,32 @@ public class NotificationService extends Service implements Recommender.Listener @Override public void handleMessage(Message msg, @NonNull NotificationService notificationService) { switch (msg.what) { - case MSG_INITIALIZE_RECOMMENDER: { - notificationService.handleInitializeRecommender(); - break; - } - case MSG_SHOW_RECOMMENDATION: { - notificationService.handleShowRecommendation(); - break; - } - case MSG_UPDATE_RECOMMENDATION: { - int notificationId = msg.arg1; - Channel channel = ((Channel) msg.obj); - notificationService.handleUpdateRecommendation(notificationId, channel); - break; - } - case MSG_HIDE_RECOMMENDATION: { - notificationService.handleHideRecommendation(); - break; - } - default: { - super.handleMessage(msg); - } + case MSG_INITIALIZE_RECOMMENDER: + { + notificationService.handleInitializeRecommender(); + break; + } + case MSG_SHOW_RECOMMENDATION: + { + notificationService.handleShowRecommendation(); + break; + } + case MSG_UPDATE_RECOMMENDATION: + { + int notificationId = msg.arg1; + Channel channel = ((Channel) msg.obj); + notificationService.handleUpdateRecommendation(notificationId, channel); + break; + } + case MSG_HIDE_RECOMMENDATION: + { + notificationService.handleHideRecommendation(); + break; + } + default: + { + super.handleMessage(msg); + } } } } diff --git a/src/com/android/tv/recommendation/RecentChannelEvaluator.java b/src/com/android/tv/recommendation/RecentChannelEvaluator.java index e724f4ce..f4c4877d 100644 --- a/src/com/android/tv/recommendation/RecentChannelEvaluator.java +++ b/src/com/android/tv/recommendation/RecentChannelEvaluator.java @@ -51,9 +51,12 @@ public class RecentChannelEvaluator extends Recommender.Evaluator { if (watchDuration < WATCH_DURATION_MS_LOWER_BOUND) { watchDurationScore = MAX_SCORE_FOR_LOWER_BOUND; } else if (watchDuration < WATCH_DURATION_MS_UPPER_BOUND) { - watchDurationScore = (watchDuration - WATCH_DURATION_MS_LOWER_BOUND) - / (WATCH_DURATION_MS_UPPER_BOUND - WATCH_DURATION_MS_LOWER_BOUND) - * (1 - MAX_SCORE_FOR_LOWER_BOUND) + MAX_SCORE_FOR_LOWER_BOUND; + watchDurationScore = + (watchDuration - WATCH_DURATION_MS_LOWER_BOUND) + / (WATCH_DURATION_MS_UPPER_BOUND + - WATCH_DURATION_MS_LOWER_BOUND) + * (1 - MAX_SCORE_FOR_LOWER_BOUND) + + MAX_SCORE_FOR_LOWER_BOUND; } else { watchDurationScore = 1.0; } @@ -61,4 +64,4 @@ public class RecentChannelEvaluator extends Recommender.Evaluator { } return (maxScore > 0.0) ? maxScore : NOT_RECOMMENDED; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index dc148ec8..649920fb 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -33,16 +33,14 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.data.WatchedHistoryManager; -import com.android.tv.util.PermissionUtils; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvUriMatcher; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -52,6 +50,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +/** Manages teh data need to make recommendations. */ public class RecommendationDataManager implements WatchedHistoryManager.Listener { private static final int MSG_START = 1000; private static final int MSG_STOP = 1001; @@ -84,40 +83,38 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private final HandlerThread mHandlerThread; private final Handler mHandler; private final Handler mMainHandler; - @Nullable - private WatchedHistoryManager mWatchedHistoryManager; + @Nullable private WatchedHistoryManager mWatchedHistoryManager; private final ChannelDataManager mChannelDataManager; private final ChannelDataManager.Listener mChannelDataListener = new ChannelDataManager.Listener() { - @Override - @MainThread - public void onLoadFinished() { - updateChannelData(); - } + @Override + @MainThread + public void onLoadFinished() { + updateChannelData(); + } - @Override - @MainThread - public void onChannelListUpdated() { - updateChannelData(); - } + @Override + @MainThread + public void onChannelListUpdated() { + updateChannelData(); + } - @Override - @MainThread - public void onChannelBrowsableChanged() { - updateChannelData(); - } - }; + @Override + @MainThread + public void onChannelBrowsableChanged() { + updateChannelData(); + } + }; // For thread safety, this variable is handled only on main thread. private final List<Listener> mListeners = new ArrayList<>(); /** - * Gets instance of RecommendationDataManager, and adds a {@link Listener}. - * The listener methods will be called in the same thread as its caller of the method. - * Note that {@link #release(Listener)} should be called when this manager is not needed - * any more. + * Gets instance of RecommendationDataManager, and adds a {@link Listener}. The listener methods + * will be called in the same thread as its caller of the method. Note that {@link + * #release(Listener)} should be called when this manager is not needed any more. */ - public synchronized static RecommendationDataManager acquireManager( + public static synchronized RecommendationDataManager acquireManager( Context context, @NonNull Listener listener) { if (sManager == null) { sManager = new RecommendationDataManager(context); @@ -129,7 +126,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override - public void onInputStateChanged(String inputId, int state) { } + public void onInputStateChanged(String inputId, int state) {} @Override public void onInputAdded(String inputId) { @@ -144,8 +141,8 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener for (ChannelRecord channelRecord : mChannelRecordMap.values()) { if (channelRecord.getChannel().getInputId().equals(inputId)) { channelRecord.setInputRemoved(false); - mAvailableChannelRecordMap.put(channelRecord.getChannel().getId(), - channelRecord); + mAvailableChannelRecordMap.put( + channelRecord.getChannel().getId(), channelRecord); channelRecordMapChanged = true; } } @@ -179,7 +176,7 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } @Override - public void onInputUpdated(String inputId) { } + public void onInputUpdated(String inputId) {} }; private RecommendationDataManager(Context context) { @@ -189,48 +186,44 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this); mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this); mContentObserver = new RecommendationContentObserver(mHandler); - mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); - runOnMainThread(new Runnable() { - @Override - public void run() { - start(); - } - }); + mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager(); + runOnMainThread( + new Runnable() { + @Override + public void run() { + start(); + } + }); } /** - * Removes the {@link Listener}, and releases RecommendationDataManager - * if there are no listeners remained. + * Removes the {@link Listener}, and releases RecommendationDataManager if there are no + * listeners remained. */ public void release(@NonNull final Listener listener) { - runOnMainThread(new Runnable() { - @Override - public void run() { - removeListener(listener); - if (mListeners.size() == 0) { - stop(); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + removeListener(listener); + if (mListeners.size() == 0) { + stop(); + } + } + }); } - /** - * Returns a {@link ChannelRecord} corresponds to the channel ID {@code ChannelId}. - */ + /** Returns a {@link ChannelRecord} corresponds to the channel ID {@code ChannelId}. */ public ChannelRecord getChannelRecord(long channelId) { return mAvailableChannelRecordMap.get(channelId); } - /** - * Returns the number of channels registered in ChannelRecord map. - */ + /** Returns the number of channels registered in ChannelRecord map. */ public int getChannelRecordCount() { return mAvailableChannelRecordMap.size(); } - /** - * Returns a Collection of ChannelRecords. - */ + /** Returns a Collection of ChannelRecords. */ public Collection<ChannelRecord> getChannelRecords() { return Collections.unmodifiableCollection(mAvailableChannelRecordMap.values()); } @@ -264,12 +257,13 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } private void addListener(Listener listener) { - runOnMainThread(new Runnable() { - @Override - public void run() { - mListeners.add(listener); - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + mListeners.add(listener); + } + }); } @MainThread @@ -286,10 +280,11 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener mWatchedHistoryManager.setListener(this); mWatchedHistoryManager.start(); } else { - mContext.getContentResolver().registerContentObserver( - TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); - mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, - TvContract.WatchedPrograms.CONTENT_URI) + mContext.getContentResolver() + .registerContentObserver( + TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); + mHandler.obtainMessage( + MSG_UPDATE_WATCH_HISTORY, TvContract.WatchedPrograms.CONTENT_URI) .sendToTarget(); } mTvInputManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE); @@ -333,7 +328,8 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } } } - if (isChannelRecordMapChanged && mChannelRecordMapLoaded + if (isChannelRecordMapChanged + && mChannelRecordMapLoaded && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) { mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED); } @@ -356,14 +352,15 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(watchedProgram); if (mChannelRecordMapLoaded && channelRecord != null) { - runOnMainThread(new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onNewWatchLog(channelRecord); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); + } + } + }); } } if (!mChannelRecordMapLoaded) { @@ -374,96 +371,99 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener private WatchedProgram convertFromWatchedHistoryManagerRecords( WatchedHistoryManager.WatchedRecord watchedRecord) { long endTime = watchedRecord.watchedStartTime + watchedRecord.duration; - Program program = new Program.Builder() - .setChannelId(watchedRecord.channelId) - .setTitle("") - .setStartTimeUtcMillis(watchedRecord.watchedStartTime) - .setEndTimeUtcMillis(endTime) - .build(); + Program program = + new Program.Builder() + .setChannelId(watchedRecord.channelId) + .setTitle("") + .setStartTimeUtcMillis(watchedRecord.watchedStartTime) + .setEndTimeUtcMillis(endTime) + .build(); return new WatchedProgram(program, watchedRecord.watchedStartTime, endTime); } @Override public void onLoadFinished() { - for (WatchedHistoryManager.WatchedRecord record - : mWatchedHistoryManager.getWatchedHistory()) { - updateChannelRecordFromWatchedProgram( - convertFromWatchedHistoryManagerRecords(record)); + for (WatchedHistoryManager.WatchedRecord record : + mWatchedHistoryManager.getWatchedHistory()) { + updateChannelRecordFromWatchedProgram(convertFromWatchedHistoryManagerRecords(record)); } mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED); } @Override public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) { - final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram( - convertFromWatchedHistoryManagerRecords(watchedRecord)); + final ChannelRecord channelRecord = + updateChannelRecordFromWatchedProgram( + convertFromWatchedHistoryManagerRecords(watchedRecord)); if (mChannelRecordMapLoaded && channelRecord != null) { - runOnMainThread(new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onNewWatchLog(channelRecord); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onNewWatchLog(channelRecord); + } + } + }); } } private WatchedProgram createWatchedProgramFromWatchedProgramCursor(Cursor cursor) { // Have to initiate the indexes of WatchedProgram Columns. if (mIndexWatchChannelId == -1) { - mIndexWatchChannelId = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_CHANNEL_ID); - mIndexProgramTitle = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_TITLE); - mIndexProgramStartTime = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); - mIndexProgramEndTime = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); - mIndexWatchStartTime = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); - mIndexWatchEndTime = cursor.getColumnIndex( - TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); + mIndexWatchChannelId = + cursor.getColumnIndex(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID); + mIndexProgramTitle = cursor.getColumnIndex(TvContract.WatchedPrograms.COLUMN_TITLE); + mIndexProgramStartTime = + cursor.getColumnIndex(TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); + mIndexProgramEndTime = + cursor.getColumnIndex(TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); + mIndexWatchStartTime = + cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); + mIndexWatchEndTime = + cursor.getColumnIndex( + TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); } - Program program = new Program.Builder() - .setChannelId(cursor.getLong(mIndexWatchChannelId)) - .setTitle(cursor.getString(mIndexProgramTitle)) - .setStartTimeUtcMillis(cursor.getLong(mIndexProgramStartTime)) - .setEndTimeUtcMillis(cursor.getLong(mIndexProgramEndTime)) - .build(); + Program program = + new Program.Builder() + .setChannelId(cursor.getLong(mIndexWatchChannelId)) + .setTitle(cursor.getString(mIndexProgramTitle)) + .setStartTimeUtcMillis(cursor.getLong(mIndexProgramStartTime)) + .setEndTimeUtcMillis(cursor.getLong(mIndexProgramEndTime)) + .build(); - return new WatchedProgram(program, - cursor.getLong(mIndexWatchStartTime), - cursor.getLong(mIndexWatchEndTime)); + return new WatchedProgram( + program, cursor.getLong(mIndexWatchStartTime), cursor.getLong(mIndexWatchEndTime)); } private void onNotifyChannelRecordMapLoaded() { mChannelRecordMapLoaded = true; - runOnMainThread(new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onChannelRecordLoaded(); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onChannelRecordLoaded(); + } + } + }); } private void onNotifyChannelRecordMapChanged() { - runOnMainThread(new Runnable() { - @Override - public void run() { - for (Listener l : mListeners) { - l.onChannelRecordChanged(); - } - } - }); + runOnMainThread( + new Runnable() { + @Override + public void run() { + for (Listener l : mListeners) { + l.onChannelRecordChanged(); + } + } + }); } - /** - * Returns true if ChannelRecords are added into mChannelRecordMap or removed from it. - */ + /** Returns true if ChannelRecords are added into mChannelRecordMap or removed from it. */ private boolean updateChannelRecordMapFromChannel(Channel channel) { if (!channel.isBrowsable()) { mChannelRecordMap.remove(channel.getId()); @@ -507,8 +507,8 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener public void onChange(final boolean selfChange, final Uri uri) { switch (TvUriMatcher.match(uri)) { case TvUriMatcher.MATCH_WATCHED_PROGRAM_ID: - if (!mHandler.hasMessages(MSG_UPDATE_WATCH_HISTORY, - TvContract.WatchedPrograms.CONTENT_URI)) { + if (!mHandler.hasMessages( + MSG_UPDATE_WATCH_HISTORY, TvContract.WatchedPrograms.CONTENT_URI)) { mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, uri).sendToTarget(); } break; @@ -524,15 +524,11 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } } - /** - * A listener interface to receive notification about the recommendation data. - * - * @MainThread - */ + /** A listener interface to receive notification about the recommendation data. @MainThread */ public interface Listener { /** - * Called when loading channel record map from database is finished. - * It will be called after RecommendationDataManager.start() is finished. + * Called when loading channel record map from database is finished. It will be called after + * RecommendationDataManager.start() is finished. * * <p>Note that this method is called on the main thread. */ @@ -601,6 +597,6 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener } @Override - protected void handleMessage(Message msg, @NonNull RecommendationDataManager referent) { } + protected void handleMessage(Message msg, @NonNull RecommendationDataManager referent) {} } } diff --git a/src/com/android/tv/recommendation/Recommender.java b/src/com/android/tv/recommendation/Recommender.java index 82c2893d..f350799f 100644 --- a/src/com/android/tv/recommendation/Recommender.java +++ b/src/com/android/tv/recommendation/Recommender.java @@ -20,9 +20,7 @@ import android.content.Context; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.Pair; - -import com.android.tv.data.Channel; - +import com.android.tv.data.api.Channel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -35,8 +33,7 @@ import java.util.concurrent.TimeUnit; public class Recommender implements RecommendationDataManager.Listener { private static final String TAG = "Recommender"; - @VisibleForTesting - static final String INVALID_CHANNEL_SORT_KEY = "INVALID"; + @VisibleForTesting static final String INVALID_CHANNEL_SORT_KEY = "INVALID"; private static final long MINIMUM_RECOMMENDATION_UPDATE_PERIOD = TimeUnit.MINUTES.toMillis(5); private static final Comparator<Pair<Channel, Double>> mChannelScoreComparator = new Comparator<Pair<Channel, Double>>() { @@ -69,7 +66,9 @@ public class Recommender implements RecommendationDataManager.Listener { } @VisibleForTesting - Recommender(Listener listener, boolean includeRecommendedOnly, + Recommender( + Listener listener, + boolean includeRecommendedOnly, RecommendationDataManager dataManager) { mListener = listener; mIncludeRecommendedOnly = includeRecommendedOnly; @@ -85,16 +84,16 @@ public class Recommender implements RecommendationDataManager.Listener { } public void registerEvaluator(Evaluator evaluator) { - registerEvaluator(evaluator, - EvaluatorWrapper.DEFAULT_BASE_SCORE, EvaluatorWrapper.DEFAULT_WEIGHT); + registerEvaluator( + evaluator, EvaluatorWrapper.DEFAULT_BASE_SCORE, EvaluatorWrapper.DEFAULT_WEIGHT); } /** * Register the evaluator used in recommendation. * - * The range of evaluated scores by this evaluator will be between {@code baseScore} and + * <p>The range of evaluated scores by this evaluator will be between {@code baseScore} and * {@code baseScore} + {@code weight} (inclusive). - + * * @param evaluator The evaluator to register inside this recommender. * @param baseScore Base(Minimum) score of the score evaluated by {@code evaluator}. * @param weight Weight value to rearrange the score evaluated by {@code evaluator}. @@ -108,13 +107,13 @@ public class Recommender implements RecommendationDataManager.Listener { } /** - * Return the channel list of recommendation up to {@code n} or the number of channels. - * During the evaluation, this method updates the channel sort key of recommended channels. + * Return the channel list of recommendation up to {@code n} or the number of channels. During + * the evaluation, this method updates the channel sort key of recommended channels. * * @param size The number of channels that might be recommended. - * @return Top {@code size} channels recommended sorted by score in descending order. If - * {@code size} is bigger than the number of channels, the number of results could - * be less than {@code size}. + * @return Top {@code size} channels recommended sorted by score in descending order. If {@code + * size} is bigger than the number of channels, the number of results could be less than + * {@code size}. */ public List<Channel> recommendChannels(int size) { List<Pair<Channel, Double>> records = new ArrayList<>(); @@ -154,7 +153,7 @@ public class Recommender implements RecommendationDataManager.Listener { * * @param channelId The channel ID to retrieve the {@link Channel} object for. * @return the {@link Channel} object for the given channel ID, {@code null} if such a channel - * is not found. + * is not found. */ public Channel getChannel(long channelId) { ChannelRecord record = mDataManager.getChannelRecord(channelId); @@ -172,10 +171,10 @@ public class Recommender implements RecommendationDataManager.Listener { } /** - * Returns the sort key of a given channel Id. Sort key is determined in - * {@link #recommendChannels()} and getChannelSortKey must be called after that. + * Returns the sort key of a given channel Id. Sort key is determined in {@link + * #recommendChannels()} and getChannelSortKey must be called after that. * - * If getChannelSortKey was called before evaluating the channels or trying to get sort key + * <p>If getChannelSortKey was called before evaluating the channels or trying to get sort key * of non-recommended channel, it returns {@link #INVALID_CHANNEL_SORT_KEY}. */ public String getChannelSortKey(long channelId) { @@ -231,27 +230,25 @@ public class Recommender implements RecommendationDataManager.Listener { mLastRecommendationUpdatedTimeUtcMillis = newUpdatedTimeMs; } - public static abstract class Evaluator { + public abstract static class Evaluator { public static final double NOT_RECOMMENDED = -1.0; private Recommender mRecommender; protected Evaluator() {} - protected void onChannelRecordListChanged(List<ChannelRecord> channelRecords) { - } + protected void onChannelRecordListChanged(List<ChannelRecord> channelRecords) {} /** * This will be called when a new watch log comes into WatchedPrograms table. * * @param channelRecord The channel record corresponds to the new watch log. */ - protected void onNewWatchLog(ChannelRecord channelRecord) { - } + protected void onNewWatchLog(ChannelRecord channelRecord) {} /** - * The implementation should return the recommendation score for the given channel ID. - * The return value should be in the range of [0.0, 1.0] or NOT_RECOMMENDED for denoting - * that it gives up to calculate the score for the channel. + * The implementation should return the recommendation score for the given channel ID. The + * return value should be in the range of [0.0, 1.0] or NOT_RECOMMENDED for denoting that it + * gives up to calculate the score for the channel. * * @param channelId The channel ID which will be evaluated by this recommender. * @return The recommendation score @@ -278,8 +275,8 @@ public class Recommender implements RecommendationDataManager.Listener { // this value. private final double mWeight; - public EvaluatorWrapper(Recommender recommender, Evaluator evaluator, - double baseScore, double weight) { + public EvaluatorWrapper( + Recommender recommender, Evaluator evaluator, double baseScore, double weight) { mEvaluator = evaluator; evaluator.setRecommender(recommender); mBaseScore = baseScore; @@ -287,27 +284,27 @@ public class Recommender implements RecommendationDataManager.Listener { } /** - * This returns the scaled score for the given channel ID based on the returned value - * of evaluateChannel(). + * This returns the scaled score for the given channel ID based on the returned value of + * evaluateChannel(). * * @param channelId The channel ID which will be evaluated by the recommender. * @return Returns the scaled score (mBaseScore + score * mWeight) when evaluateChannel() is - * in the range of [0.0, 1.0]. If evaluateChannel() returns NOT_RECOMMENDED or any - * negative numbers, it returns NOT_RECOMMENDED. If calculateScore() returns more - * than 1.0, it returns (mBaseScore + mWeight). + * in the range of [0.0, 1.0]. If evaluateChannel() returns NOT_RECOMMENDED or any + * negative numbers, it returns NOT_RECOMMENDED. If calculateScore() returns more than + * 1.0, it returns (mBaseScore + mWeight). */ private double getScaledEvaluatorScore(long channelId) { double score = mEvaluator.evaluateChannel(channelId); if (score < 0.0) { if (score != Evaluator.NOT_RECOMMENDED) { - Log.w(TAG, "Unexpected score (" + score + ") from the recommender" - + mEvaluator); + Log.w( + TAG, + "Unexpected score (" + score + ") from the recommender" + mEvaluator); } // If the recommender gives up to calculate the score, return 0.0 return Evaluator.NOT_RECOMMENDED; } else if (score > 1.0) { - Log.w(TAG, "Unexpected score (" + score + ") from the recommender" - + mEvaluator); + Log.w(TAG, "Unexpected score (" + score + ") from the recommender" + mEvaluator); score = 1.0; } return mBaseScore + score * mWeight; @@ -323,14 +320,10 @@ public class Recommender implements RecommendationDataManager.Listener { } public interface Listener { - /** - * Called after channel record map is loaded. - */ + /** Called after channel record map is loaded. */ void onRecommenderReady(); - /** - * Called when the recommendation changes. - */ + /** Called when the recommendation changes. */ void onRecommendationChanged(); } } diff --git a/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java index ad55afb7..edc23c53 100644 --- a/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java +++ b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java @@ -21,39 +21,32 @@ import android.os.Build; import android.support.annotation.RequiresApi; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.data.PreviewDataManager; import com.android.tv.data.PreviewProgramContent; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.data.RecordedProgram; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Set; -/** - * Class to update the preview data for {@link RecordedProgram} - */ +/** Class to update the preview data for {@link RecordedProgram} */ @RequiresApi(Build.VERSION_CODES.O) +@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated public class RecordedProgramPreviewUpdater { private static final String TAG = "RecordedProgramPreviewUpdater"; - // STOPSHIP: set it to false. - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private static final int RECOMMENDATION_COUNT = 6; private static RecordedProgramPreviewUpdater sRecordedProgramPreviewUpdater; - /** - * Creates and returns the {@link RecordedProgramPreviewUpdater}. - */ + /** Creates and returns the {@link RecordedProgramPreviewUpdater}. */ public static RecordedProgramPreviewUpdater getInstance(Context context) { if (sRecordedProgramPreviewUpdater == null) { - sRecordedProgramPreviewUpdater - = new RecordedProgramPreviewUpdater(context.getApplicationContext()); + sRecordedProgramPreviewUpdater = + new RecordedProgramPreviewUpdater(context.getApplicationContext()); } return sRecordedProgramPreviewUpdater; } @@ -64,56 +57,56 @@ public class RecordedProgramPreviewUpdater { private RecordedProgramPreviewUpdater(Context context) { mContext = context.getApplicationContext(); - ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext); - mPreviewDataManager = applicationSingletons.getPreviewDataManager(); - mDvrDataManager = applicationSingletons.getDvrDataManager(); - mDvrDataManager.addRecordedProgramListener(new DvrDataManager.RecordedProgramListener() { - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - if (DEBUG) Log.d(TAG, "Add new preview recorded programs"); - updatePreviewDataForRecordedPrograms(); - } + TvSingletons tvSingletons = TvSingletons.getSingletons(mContext); + mPreviewDataManager = tvSingletons.getPreviewDataManager(); + mDvrDataManager = tvSingletons.getDvrDataManager(); + mDvrDataManager.addRecordedProgramListener( + new DvrDataManager.RecordedProgramListener() { + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + if (DEBUG) Log.d(TAG, "Add new preview recorded programs"); + updatePreviewDataForRecordedPrograms(); + } - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - if (DEBUG) Log.d(TAG, "Update preview recorded programs"); - updatePreviewDataForRecordedPrograms(); - } + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + if (DEBUG) Log.d(TAG, "Update preview recorded programs"); + updatePreviewDataForRecordedPrograms(); + } - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - if (DEBUG) Log.d(TAG, "Delete preview recorded programs"); - updatePreviewDataForRecordedPrograms(); - } - }); + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + if (DEBUG) Log.d(TAG, "Delete preview recorded programs"); + updatePreviewDataForRecordedPrograms(); + } + }); } - /** - * Updates the preview data for recorded programs. - */ + /** Updates the preview data for recorded programs. */ public void updatePreviewDataForRecordedPrograms() { if (!mPreviewDataManager.isLoadFinished()) { - mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() { - @Override - public void onPreviewDataLoadFinished() { - mPreviewDataManager.removeListener(this); - updatePreviewDataForRecordedPrograms(); - } + mPreviewDataManager.addListener( + new PreviewDataManager.PreviewDataListener() { + @Override + public void onPreviewDataLoadFinished() { + mPreviewDataManager.removeListener(this); + updatePreviewDataForRecordedPrograms(); + } - @Override - public void onPreviewDataUpdateFinished() { } - }); + @Override + public void onPreviewDataUpdateFinished() {} + }); return; } if (!mDvrDataManager.isRecordedProgramLoadFinished()) { mDvrDataManager.addRecordedProgramLoadFinishedListener( new DvrDataManager.OnRecordedProgramLoadFinishedListener() { - @Override - public void onRecordedProgramLoadFinished() { - mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); - updatePreviewDataForRecordedPrograms(); - } - }); + @Override + public void onRecordedProgramLoadFinished() { + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + updatePreviewDataForRecordedPrograms(); + } + }); return; } updatePreviewDataForRecordedProgramsInternal(); @@ -121,15 +114,18 @@ public class RecordedProgramPreviewUpdater { private void updatePreviewDataForRecordedProgramsInternal() { Set<RecordedProgram> recordedPrograms = generateRecommendationRecordedPrograms(); - Long recordedPreviewChannelId = mPreviewDataManager.getPreviewChannelId( - PreviewDataManager.TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL); + Long recordedPreviewChannelId = + mPreviewDataManager.getPreviewChannelId( + PreviewDataManager.TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL); if (recordedPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID && !recordedPrograms.isEmpty()) { createPreviewChannelForRecordedPrograms(); } else { - mPreviewDataManager.updatePreviewProgramsForChannel(recordedPreviewChannelId, + mPreviewDataManager.updatePreviewProgramsForChannel( + recordedPreviewChannelId, generatePreviewProgramContentsFromRecordedPrograms( - recordedPreviewChannelId, recordedPrograms), null); + recordedPreviewChannelId, recordedPrograms), + null); } } @@ -168,8 +164,9 @@ public class RecordedProgramPreviewUpdater { long previewChannelId, Set<RecordedProgram> recordedPrograms) { Set<PreviewProgramContent> result = new HashSet<>(); for (RecordedProgram recordedProgram : recordedPrograms) { - result.add(PreviewProgramContent.createFromRecordedProgram(mContext, previewChannelId, - recordedProgram)); + result.add( + PreviewProgramContent.createFromRecordedProgram( + mContext, previewChannelId, recordedProgram)); } return result; } diff --git a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java index 5ff7cae9..9240682a 100644 --- a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java +++ b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java @@ -19,9 +19,7 @@ package com.android.tv.recommendation; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; - import com.android.tv.data.Program; - import java.text.BreakIterator; import java.util.ArrayList; import java.util.Calendar; @@ -32,8 +30,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { // TODO: test and refine constant values in WatchedProgramRecommender in order to // improve the performance of this recommender. private static final double REQUIRED_MIN_SCORE = 0.15; - @VisibleForTesting - static final double MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK = 0.7; + @VisibleForTesting static final double MULTIPLIER_FOR_UNMATCHED_DAY_OF_WEEK = 0.7; private static final double TITLE_MATCH_WEIGHT = 0.5; private static final double TIME_MATCH_WEIGHT = 1 - TITLE_MATCH_WEIGHT; private static final long DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM = TimeUnit.DAYS.toMillis(14); @@ -57,8 +54,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { } Program watchedProgram = watchHistory[watchHistory.length - 1].getProgram(); - long startTimeDiffMsWithCurrentProgram = currentProgram.getStartTimeUtcMillis() - - watchedProgram.getStartTimeUtcMillis(); + long startTimeDiffMsWithCurrentProgram = + currentProgram.getStartTimeUtcMillis() - watchedProgram.getStartTimeUtcMillis(); if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) { return NOT_RECOMMENDED; } @@ -70,42 +67,48 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { == watchHistory[i].getProgram().getStartTimeUtcMillis()) { watchedDurationMs += watchHistory[i].getWatchedDurationMs(); } else { - double score = calculateRoutineWatchScore( - currentProgram, watchedProgram, watchedDurationMs); + double score = + calculateRoutineWatchScore( + currentProgram, watchedProgram, watchedDurationMs); if (score >= REQUIRED_MIN_SCORE && score > maxScore) { maxScore = score; } watchedProgram = watchHistory[i].getProgram(); watchedDurationMs = watchHistory[i].getWatchedDurationMs(); - startTimeDiffMsWithCurrentProgram = currentProgram.getStartTimeUtcMillis() - - watchedProgram.getStartTimeUtcMillis(); + startTimeDiffMsWithCurrentProgram = + currentProgram.getStartTimeUtcMillis() + - watchedProgram.getStartTimeUtcMillis(); if (startTimeDiffMsWithCurrentProgram >= MAX_DIFF_MS_FOR_OLD_PROGRAM) { return maxScore; } } } - double score = calculateRoutineWatchScore( - currentProgram, watchedProgram, watchedDurationMs); + double score = + calculateRoutineWatchScore(currentProgram, watchedProgram, watchedDurationMs); if (score >= REQUIRED_MIN_SCORE && score > maxScore) { maxScore = score; } return maxScore; } - private static double calculateRoutineWatchScore(Program currentProgram, Program watchedProgram, - long watchedDurationMs) { + private static double calculateRoutineWatchScore( + Program currentProgram, Program watchedProgram, long watchedDurationMs) { double timeMatchScore = calculateTimeMatchScore(currentProgram, watchedProgram); - double titleMatchScore = calculateTitleMatchScore( - currentProgram.getTitle(), watchedProgram.getTitle()); + double titleMatchScore = + calculateTitleMatchScore(currentProgram.getTitle(), watchedProgram.getTitle()); double watchDurationScore = calculateWatchDurationScore(watchedProgram, watchedDurationMs); - long diffMs = currentProgram.getStartTimeUtcMillis() - - watchedProgram.getStartTimeUtcMillis(); - double multiplierForOldProgram = (diffMs < MAX_DIFF_MS_FOR_OLD_PROGRAM) - ? 1.0 - (double) Math.max(diffMs - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM, 0) - / (MAX_DIFF_MS_FOR_OLD_PROGRAM - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM) - : 0.0; + long diffMs = + currentProgram.getStartTimeUtcMillis() - watchedProgram.getStartTimeUtcMillis(); + double multiplierForOldProgram = + (diffMs < MAX_DIFF_MS_FOR_OLD_PROGRAM) + ? 1.0 + - (double) Math.max(diffMs - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM, 0) + / (MAX_DIFF_MS_FOR_OLD_PROGRAM + - DIFF_MS_TOLERANCE_FOR_OLD_PROGRAM) + : 0.0; return (titleMatchScore * TITLE_MATCH_WEIGHT + timeMatchScore * TIME_MATCH_WEIGHT) - * watchDurationScore * multiplierForOldProgram; + * watchDurationScore + * multiplierForOldProgram; } @VisibleForTesting @@ -118,8 +121,7 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { if (wordList1.isEmpty() || wordList2.isEmpty()) { return 0; } - int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength( - wordList1, wordList2); + int maxMatchedWordSeqLen = calculateMaximumMatchedWordSequenceLength(wordList1, wordList2); // F-measure score double precision = (double) maxMatchedWordSeqLen / wordList1.size(); @@ -128,8 +130,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { } @VisibleForTesting - static int calculateMaximumMatchedWordSequenceLength(List<String> toSearchWords, - List<String> toMatchWords) { + static int calculateMaximumMatchedWordSequenceLength( + List<String> toSearchWords, List<String> toMatchWords) { int[] matchedWordSeqLen = new int[toMatchWords.size()]; int maxMatchedWordSeqLen = 0; for (String word : toSearchWords) { @@ -170,14 +172,20 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { boolean sameDay = false; // Handle cases like (00:00 - 02:00) - (01:00 - 03:00) or (22:00 - 25:00) - (23:00 - 26:00). - double score = Math.max(0, Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec) - - Math.max(t1.startTimeOfDayInSec, t2.startTimeOfDayInSec)); + double score = + Math.max( + 0, + Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec) + - Math.max(t1.startTimeOfDayInSec, t2.startTimeOfDayInSec)); if (score > 0) { sameDay = (t1.weekDay == t2.weekDay); } else if (t1.dayChanged != t2.dayChanged) { // To handle cases like t1 : (00:00 - 01:00) and t2 : (23:00 - 25:00). - score = Math.max(0, Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec - 24 * 60 * 60) - - t1.startTimeOfDayInSec); + score = + Math.max( + 0, + Math.min(t1.endTimeOfDayInSec, t2.endTimeOfDayInSec - 24 * 60 * 60) + - t1.startTimeOfDayInSec); // Same day if next day of t2's start day equals to t1's start day. (1 <= weekDay <= 7) sameDay = (t1.weekDay == ((t2.weekDay % 7) + 1)); } @@ -206,7 +214,8 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { BreakIterator boundary = BreakIterator.getWordInstance(); boundary.setText(text); int start = boundary.first(); - for (int end = boundary.next(); end != BreakIterator.DONE; + for (int end = boundary.next(); + end != BreakIterator.DONE; start = end, end = boundary.next()) { String word = text.substring(start, end); if (Character.isLetterOrDigit(word.charAt(0))) { @@ -233,15 +242,20 @@ public class RoutineWatchEvaluator extends Recommender.Evaluator { time.setTimeInMillis(p.getEndTimeUtcMillis()); boolean dayChanged = (weekDay != time.get(Calendar.DAY_OF_WEEK)); // Set maximum program duration time to 12 hours. - int endTimeOfDayInSec = startTimeOfDayInSec + - (int) Math.min(p.getEndTimeUtcMillis() - p.getStartTimeUtcMillis(), - TimeUnit.HOURS.toMillis(12)) / 1000; + int endTimeOfDayInSec = + startTimeOfDayInSec + + (int) + Math.min( + p.getEndTimeUtcMillis() + - p.getStartTimeUtcMillis(), + TimeUnit.HOURS.toMillis(12)) + / 1000; return new ProgramTime(startTimeOfDayInSec, endTimeOfDayInSec, weekDay, dayChanged); } - private ProgramTime(int startTimeOfDayInSec, int endTimeOfDayInSec, int weekDay, - boolean dayChanged) { + private ProgramTime( + int startTimeOfDayInSec, int endTimeOfDayInSec, int weekDay, boolean dayChanged) { this.startTimeOfDayInSec = startTimeOfDayInSec; this.endTimeOfDayInSec = endTimeOfDayInSec; this.weekDay = weekDay; diff --git a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java new file mode 100644 index 00000000..528096dd --- /dev/null +++ b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + + + +package com.android.tv.search; + +import android.support.annotation.Nullable; + +/** + * Hand copy of generated Autovalue class. + * + * TODO get autovalue working + */ + +final class AutoValue_LocalSearchProvider_SearchResult extends LocalSearchProvider.SearchResult { + + private final long channelId; + private final String channelNumber; + private final String title; + private final String description; + private final String imageUri; + private final String intentAction; + private final String intentData; + private final String intentExtraData; + private final String contentType; + private final boolean isLive; + private final int videoWidth; + private final int videoHeight; + private final long duration; + private final int progressPercentage; + + private AutoValue_LocalSearchProvider_SearchResult( + long channelId, + @Nullable String channelNumber, + @Nullable String title, + @Nullable String description, + @Nullable String imageUri, + @Nullable String intentAction, + @Nullable String intentData, + @Nullable String intentExtraData, + @Nullable String contentType, + boolean isLive, + int videoWidth, + int videoHeight, + long duration, + int progressPercentage) { + this.channelId = channelId; + this.channelNumber = channelNumber; + this.title = title; + this.description = description; + this.imageUri = imageUri; + this.intentAction = intentAction; + this.intentData = intentData; + this.intentExtraData = intentExtraData; + this.contentType = contentType; + this.isLive = isLive; + this.videoWidth = videoWidth; + this.videoHeight = videoHeight; + this.duration = duration; + this.progressPercentage = progressPercentage; + } + + @Override + long getChannelId() { + return channelId; + } + + @Nullable + @Override + String getChannelNumber() { + return channelNumber; + } + + @Nullable + @Override + String getTitle() { + return title; + } + + @Nullable + @Override + String getDescription() { + return description; + } + + @Nullable + @Override + String getImageUri() { + return imageUri; + } + + @Nullable + @Override + String getIntentAction() { + return intentAction; + } + + @Nullable + @Override + String getIntentData() { + return intentData; + } + + @Nullable + @Override + String getIntentExtraData() { + return intentExtraData; + } + + @Nullable + @Override + String getContentType() { + return contentType; + } + + @Override + boolean getIsLive() { + return isLive; + } + + @Override + int getVideoWidth() { + return videoWidth; + } + + @Override + int getVideoHeight() { + return videoHeight; + } + + @Override + long getDuration() { + return duration; + } + + @Override + int getProgressPercentage() { + return progressPercentage; + } + + @Override + public String toString() { + return "SearchResult{" + + "channelId=" + channelId + ", " + + "channelNumber=" + channelNumber + ", " + + "title=" + title + ", " + + "description=" + description + ", " + + "imageUri=" + imageUri + ", " + + "intentAction=" + intentAction + ", " + + "intentData=" + intentData + ", " + + "intentExtraData=" + intentExtraData + ", " + + "contentType=" + contentType + ", " + + "isLive=" + isLive + ", " + + "videoWidth=" + videoWidth + ", " + + "videoHeight=" + videoHeight + ", " + + "duration=" + duration + ", " + + "progressPercentage=" + progressPercentage + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof LocalSearchProvider.SearchResult) { + LocalSearchProvider.SearchResult that = (LocalSearchProvider.SearchResult) o; + return (this.channelId == that.getChannelId()) + && ((this.channelNumber == null) ? (that.getChannelNumber() == null) : this.channelNumber.equals(that.getChannelNumber())) + && ((this.title == null) ? (that.getTitle() == null) : this.title.equals(that.getTitle())) + && ((this.description == null) ? (that.getDescription() == null) : this.description.equals(that.getDescription())) + && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri())) + && ((this.intentAction == null) ? (that.getIntentAction() == null) : this.intentAction.equals(that.getIntentAction())) + && ((this.intentData == null) ? (that.getIntentData() == null) : this.intentData.equals(that.getIntentData())) + && ((this.intentExtraData == null) ? (that.getIntentExtraData() == null) : this.intentExtraData.equals(that.getIntentExtraData())) + && ((this.contentType == null) ? (that.getContentType() == null) : this.contentType.equals(that.getContentType())) + && (this.isLive == that.getIsLive()) + && (this.videoWidth == that.getVideoWidth()) + && (this.videoHeight == that.getVideoHeight()) + && (this.duration == that.getDuration()) + && (this.progressPercentage == that.getProgressPercentage()); + } + return false; + } + + @Override + public int hashCode() { + int h$ = 1; + h$ *= 1000003; + h$ ^= (int) ((channelId >>> 32) ^ channelId); + h$ *= 1000003; + h$ ^= (channelNumber == null) ? 0 : channelNumber.hashCode(); + h$ *= 1000003; + h$ ^= (title == null) ? 0 : title.hashCode(); + h$ *= 1000003; + h$ ^= (description == null) ? 0 : description.hashCode(); + h$ *= 1000003; + h$ ^= (imageUri == null) ? 0 : imageUri.hashCode(); + h$ *= 1000003; + h$ ^= (intentAction == null) ? 0 : intentAction.hashCode(); + h$ *= 1000003; + h$ ^= (intentData == null) ? 0 : intentData.hashCode(); + h$ *= 1000003; + h$ ^= (intentExtraData == null) ? 0 : intentExtraData.hashCode(); + h$ *= 1000003; + h$ ^= (contentType == null) ? 0 : contentType.hashCode(); + h$ *= 1000003; + h$ ^= isLive ? 1231 : 1237; + h$ *= 1000003; + h$ ^= videoWidth; + h$ *= 1000003; + h$ ^= videoHeight; + h$ *= 1000003; + h$ ^= (int) ((duration >>> 32) ^ duration); + h$ *= 1000003; + h$ ^= progressPercentage; + return h$; + } + + static final class Builder extends LocalSearchProvider.SearchResult.Builder { + private Long channelId; + private String channelNumber; + private String title; + private String description; + private String imageUri; + private String intentAction; + private String intentData; + private String intentExtraData; + private String contentType; + private Boolean isLive; + private Integer videoWidth; + private Integer videoHeight; + private Long duration; + private Integer progressPercentage; + Builder() { + } + @Override + LocalSearchProvider.SearchResult.Builder setChannelId(long channelId) { + this.channelId = channelId; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setChannelNumber(@Nullable String channelNumber) { + this.channelNumber = channelNumber; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setTitle(@Nullable String title) { + this.title = title; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setDescription(@Nullable String description) { + this.description = description; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setImageUri(@Nullable String imageUri) { + this.imageUri = imageUri; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setIntentAction(@Nullable String intentAction) { + this.intentAction = intentAction; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setIntentData(@Nullable String intentData) { + this.intentData = intentData; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setIntentExtraData(@Nullable String intentExtraData) { + this.intentExtraData = intentExtraData; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setContentType(@Nullable String contentType) { + this.contentType = contentType; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setIsLive(boolean isLive) { + this.isLive = isLive; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setVideoWidth(int videoWidth) { + this.videoWidth = videoWidth; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setVideoHeight(int videoHeight) { + this.videoHeight = videoHeight; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setDuration(long duration) { + this.duration = duration; + return this; + } + @Override + LocalSearchProvider.SearchResult.Builder setProgressPercentage(int progressPercentage) { + this.progressPercentage = progressPercentage; + return this; + } + @Override + LocalSearchProvider.SearchResult build() { + String missing = ""; + if (this.channelId == null) { + missing += " channelId"; + } + if (this.isLive == null) { + missing += " isLive"; + } + if (this.videoWidth == null) { + missing += " videoWidth"; + } + if (this.videoHeight == null) { + missing += " videoHeight"; + } + if (this.duration == null) { + missing += " duration"; + } + if (this.progressPercentage == null) { + missing += " progressPercentage"; + } + if (!missing.isEmpty()) { + throw new IllegalStateException("Missing required properties:" + missing); + } + return new AutoValue_LocalSearchProvider_SearchResult( + this.channelId, + this.channelNumber, + this.title, + this.description, + this.imageUri, + this.intentAction, + this.intentData, + this.intentExtraData, + this.contentType, + this.isLive, + this.videoWidth, + this.videoHeight, + this.duration, + this.progressPercentage); + } + } + +} diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java index d90908f1..82fb5016 100644 --- a/src/com/android/tv/search/DataManagerSearch.java +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -26,17 +26,14 @@ import android.os.SystemClock; import android.support.annotation.MainThread; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; +import com.android.tv.TvSingletons; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.api.Channel; import com.android.tv.search.LocalSearchProvider.SearchResult; import com.android.tv.util.MainThreadExecutor; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -47,11 +44,11 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; /** - * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager} - * and {@link ProgramDataManager}. + * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager} and + * {@link ProgramDataManager}. */ public class DataManagerSearch implements SearchInterface { - private static final String TAG = "TvProviderSearch"; + private static final String TAG = "DataManagerSearch"; private static final boolean DEBUG = false; private final Context mContext; @@ -62,20 +59,22 @@ public class DataManagerSearch implements SearchInterface { DataManagerSearch(Context context) { mContext = context; mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mChannelDataManager = appSingletons.getChannelDataManager(); - mProgramDataManager = appSingletons.getProgramDataManager(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mChannelDataManager = tvSingletons.getChannelDataManager(); + mProgramDataManager = tvSingletons.getProgramDataManager(); } @Override public List<SearchResult> search(final String query, final int limit, final int action) { - Future<List<SearchResult>> future = MainThreadExecutor.getInstance() - .submit(new Callable<List<SearchResult>>() { - @Override - public List<SearchResult> call() throws Exception { - return searchFromDataManagers(query, limit, action); - } - }); + Future<List<SearchResult>> future = + MainThreadExecutor.getInstance() + .submit( + new Callable<List<SearchResult>>() { + @Override + public List<SearchResult> call() throws Exception { + return searchFromDataManagers(query, limit, action); + } + }); try { return future.get(); @@ -90,12 +89,12 @@ public class DataManagerSearch implements SearchInterface { @MainThread private List<SearchResult> searchFromDataManagers(String query, int limit, int action) { + // TODO(b/72499165): add a test. List<SearchResult> results = new ArrayList<>(); if (!mChannelDataManager.isDbLoadFinished()) { return results; } - if (action == ACTION_TYPE_SWITCH_CHANNEL - || action == ACTION_TYPE_SWITCH_INPUT) { + if (action == ACTION_TYPE_SWITCH_CHANNEL || action == ACTION_TYPE_SWITCH_INPUT) { // Voice search query should be handled by the a system TV app. return results; } @@ -114,9 +113,14 @@ public class DataManagerSearch implements SearchInterface { } if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " channels. Elapsed time for" + - " searching channels: " + (SystemClock.elapsedRealtime() - time) + - "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " channels. Elapsed time for" + + " searching channels: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @@ -133,16 +137,27 @@ public class DataManagerSearch implements SearchInterface { } if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " channels. Elapsed time for" + - " searching channels: " + (SystemClock.elapsedRealtime() - time) + - "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " channels. Elapsed time for" + + " searching channels: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } } if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " channels. Elapsed time for" + - " searching channels: " + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " channels. Elapsed time for" + + " searching channels: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } int channelResult = results.size(); if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'"); @@ -161,9 +176,14 @@ public class DataManagerSearch implements SearchInterface { } if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + (results.size() - channelResult) + " programs. Elapsed" + - " time for searching programs: " + - (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + (results.size() - channelResult) + + " programs. Elapsed" + + " time for searching programs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @@ -182,16 +202,27 @@ public class DataManagerSearch implements SearchInterface { } if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + (results.size() - channelResult) + " programs. Elapsed" + - " time for searching programs: " + - (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + (results.size() - channelResult) + + " programs. Elapsed" + + " time for searching programs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } } if (DEBUG) { - Log.d(TAG, "Found " + (results.size() - channelResult) + " programs. Elapsed time for" + - " searching programs: " + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + (results.size() - channelResult) + + " programs. Elapsed time for" + + " searching programs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @@ -201,11 +232,9 @@ public class DataManagerSearch implements SearchInterface { return string != null && string.toLowerCase().contains(query); } - /** - * If query is matched to channel, {@code program} should be null. - */ - private void addResult(List<SearchResult> results, Set<Long> channelsFound, Channel channel, - Program program) { + /** If query is matched to channel, {@code program} should be null. */ + private void addResult( + List<SearchResult> results, Set<Long> channelsFound, Channel channel, Program program) { if (program == null) { program = mProgramDataManager.getCurrentProgram(channel.getId()); if (program != null && isRatingBlocked(program.getContentRatings())) { @@ -213,47 +242,58 @@ public class DataManagerSearch implements SearchInterface { } } - SearchResult result = new SearchResult(); + SearchResult.Builder result = SearchResult.builder(); long channelId = channel.getId(); - result.channelId = channelId; - result.channelNumber = channel.getDisplayNumber(); + result.setChannelId(channelId); + result.setChannelNumber(channel.getDisplayNumber()); if (program == null) { - result.title = channel.getDisplayName(); - result.description = channel.getDescription(); - result.imageUri = TvContract.buildChannelLogoUri(channelId).toString(); - result.intentAction = Intent.ACTION_VIEW; - result.intentData = buildIntentData(channelId); - result.contentType = Programs.CONTENT_ITEM_TYPE; - result.isLive = true; - result.progressPercentage = LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + result.setTitle(channel.getDisplayName()); + result.setDescription(channel.getDescription()); + result.setImageUri(TvContract.buildChannelLogoUri(channelId).toString()); + result.setIntentAction(Intent.ACTION_VIEW); + result.setIntentData(buildIntentData(channelId)); + result.setContentType(Programs.CONTENT_ITEM_TYPE); + result.setIsLive(true); + result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE); } else { - result.title = program.getTitle(); - result.description = buildProgramDescription(channel.getDisplayNumber(), - channel.getDisplayName(), program.getStartTimeUtcMillis(), - program.getEndTimeUtcMillis()); - result.imageUri = program.getPosterArtUri(); - result.intentAction = Intent.ACTION_VIEW; - result.intentData = buildIntentData(channelId); - result.contentType = Programs.CONTENT_ITEM_TYPE; - result.isLive = true; - result.videoWidth = program.getVideoWidth(); - result.videoHeight = program.getVideoHeight(); - result.duration = program.getDurationMillis(); - result.progressPercentage = getProgressPercentage( - program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()); + result.setTitle(program.getTitle()); + result.setDescription( + buildProgramDescription( + channel.getDisplayNumber(), + channel.getDisplayName(), + program.getStartTimeUtcMillis(), + program.getEndTimeUtcMillis())); + result.setImageUri(program.getPosterArtUri()); + result.setIntentAction(Intent.ACTION_VIEW); + result.setIntentData(buildIntentData(channelId)); + result.setIntentExtraData(TvContract.buildProgramUri(program.getId()).toString()); + result.setContentType(Programs.CONTENT_ITEM_TYPE); + result.setIsLive(true); + result.setVideoWidth(program.getVideoWidth()); + result.setVideoHeight(program.getVideoHeight()); + result.setDuration(program.getDurationMillis()); + result.setProgressPercentage( + getProgressPercentage( + program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis())); } if (DEBUG) { Log.d(TAG, "Add a result : channel=" + channel + " program=" + program); } - results.add(result); + results.add(result.build()); channelsFound.add(channel.getId()); } - private String buildProgramDescription(String channelNumber, String channelName, - long programStartUtcMillis, long programEndUtcMillis) { + private String buildProgramDescription( + String channelNumber, + String channelName, + long programStartUtcMillis, + long programEndUtcMillis) { return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false) - + System.lineSeparator() + channelNumber + " " + channelName; + + System.lineSeparator() + + channelNumber + + " " + + channelName; } private int getProgressPercentage(long startUtcMillis, long endUtcMillis) { @@ -261,7 +301,7 @@ public class DataManagerSearch implements SearchInterface { if (startUtcMillis > current || endUtcMillis <= current) { return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; } - return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); + return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); } private String buildIntentData(long channelId) { @@ -269,7 +309,8 @@ public class DataManagerSearch implements SearchInterface { } private boolean isRatingBlocked(TvContentRating[] ratings) { - if (ratings == null || ratings.length == 0 + if (ratings == null + || ratings.length == 0 || !mTvInputManager.isParentalControlsEnabled()) { return false; } diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java index ef9336d7..97e7f229 100644 --- a/src/com/android/tv/search/LocalSearchProvider.java +++ b/src/com/android/tv/search/LocalSearchProvider.java @@ -24,19 +24,19 @@ import android.database.MatrixCursor; import android.net.Uri; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; - -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; +import com.android.tv.common.CommonConstants; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.perf.EventNames; import com.android.tv.perf.PerformanceMonitor; import com.android.tv.perf.TimerEvent; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvUriMatcher; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -46,32 +46,34 @@ public class LocalSearchProvider extends ContentProvider { private static final boolean DEBUG = false; /** The authority for LocalSearchProvider. */ - public static final String AUTHORITY = "com.android.tv.search"; + public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search"; public static final int PROGRESS_PERCENTAGE_HIDE = -1; // TODO: Remove this once added to the SearchManager. private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage"; - private static final String[] SEARCHABLE_COLUMNS = new String[] { - SearchManager.SUGGEST_COLUMN_TEXT_1, - SearchManager.SUGGEST_COLUMN_TEXT_2, - SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE, - SearchManager.SUGGEST_COLUMN_INTENT_ACTION, - SearchManager.SUGGEST_COLUMN_INTENT_DATA, - SearchManager.SUGGEST_COLUMN_CONTENT_TYPE, - SearchManager.SUGGEST_COLUMN_IS_LIVE, - SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH, - SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT, - SearchManager.SUGGEST_COLUMN_DURATION, - SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE - }; + private static final String[] SEARCHABLE_COLUMNS = + new String[] { + SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE, + SearchManager.SUGGEST_COLUMN_INTENT_ACTION, + SearchManager.SUGGEST_COLUMN_INTENT_DATA, + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, + SearchManager.SUGGEST_COLUMN_CONTENT_TYPE, + SearchManager.SUGGEST_COLUMN_IS_LIVE, + SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH, + SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT, + SearchManager.SUGGEST_COLUMN_DURATION, + SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE + }; private static final String EXPECTED_PATH_PREFIX = "/" + SearchManager.SUGGEST_URI_PATH_QUERY; static final String SUGGEST_PARAMETER_ACTION = "action"; // The launcher passes 10 as a 'limit' parameter by default. - @VisibleForTesting - static final int DEFAULT_SEARCH_LIMIT = 10; + @VisibleForTesting static final int DEFAULT_SEARCH_LIMIT = 10; + @VisibleForTesting static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS; @@ -85,26 +87,41 @@ public class LocalSearchProvider extends ContentProvider { @Override public boolean onCreate() { - mPerformanceMonitor = TvApplication.getSingletons(getContext()).getPerformanceMonitor(); + mPerformanceMonitor = TvSingletons.getSingletons(getContext()).getPerformanceMonitor(); return true; } @VisibleForTesting void setSearchInterface(SearchInterface searchInterface) { - SoftPreconditions.checkState(TvCommonUtils.isRunningInTest()); + SoftPreconditions.checkState(CommonUtils.isRunningInTest()); mSearchInterface = searchInterface; } @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { + public Cursor query( + @NonNull Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { if (TvUriMatcher.match(uri) != TvUriMatcher.MATCH_ON_DEVICE_SEARCH) { throw new IllegalArgumentException("Unknown URI: " + uri); } TimerEvent queryTimer = mPerformanceMonitor.startTimer(); if (DEBUG) { - Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", " - + Arrays.toString(selectionArgs) + ", " + sortOrder + ")"); + Log.d( + TAG, + "query(" + + uri + + ", " + + Arrays.toString(projection) + + ", " + + selection + + ", " + + Arrays.toString(selectionArgs) + + ", " + + sortOrder + + ")"); } long time = SystemClock.elapsedRealtime(); SearchInterface search = mSearchInterface; @@ -118,8 +135,8 @@ public class LocalSearchProvider extends ContentProvider { } } String query = uri.getLastPathSegment(); - int limit = getQueryParamater(uri, SearchManager.SUGGEST_PARAMETER_LIMIT, - DEFAULT_SEARCH_LIMIT); + int limit = + getQueryParamater(uri, SearchManager.SUGGEST_PARAMETER_LIMIT, DEFAULT_SEARCH_LIMIT); if (limit <= 0) { limit = DEFAULT_SEARCH_LIMIT; } @@ -134,8 +151,13 @@ public class LocalSearchProvider extends ContentProvider { } Cursor c = createSuggestionsCursor(results); if (DEBUG) { - Log.d(TAG, "Elapsed time(count=" + c.getCount() + "): " - + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Elapsed time(count=" + + c.getCount() + + "): " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } mPerformanceMonitor.stopTimer(queryTimer, EventNames.ON_DEVICE_SEARCH); return c; @@ -157,17 +179,18 @@ public class LocalSearchProvider extends ContentProvider { int index = 0; for (SearchResult result : results) { row.clear(); - row.add(result.title); - row.add(result.description); - row.add(result.imageUri); - row.add(result.intentAction); - row.add(result.intentData); - row.add(result.contentType); - row.add(result.isLive ? LIVE_CONTENTS : NO_LIVE_CONTENTS); - row.add(result.videoWidth == 0 ? null : String.valueOf(result.videoWidth)); - row.add(result.videoHeight == 0 ? null : String.valueOf(result.videoHeight)); - row.add(result.duration == 0 ? null : String.valueOf(result.duration)); - row.add(String.valueOf(result.progressPercentage)); + row.add(result.getTitle()); + row.add(result.getDescription()); + row.add(result.getImageUri()); + row.add(result.getIntentAction()); + row.add(result.getIntentData()); + row.add(result.getIntentExtraData()); + row.add(result.getContentType()); + row.add(result.getIsLive() ? LIVE_CONTENTS : NO_LIVE_CONTENTS); + row.add(result.getVideoWidth() == 0 ? null : String.valueOf(result.getVideoWidth())); + row.add(result.getVideoHeight() == 0 ? null : String.valueOf(result.getVideoHeight())); + row.add(result.getDuration() == 0 ? null : String.valueOf(result.getDuration())); + row.add(String.valueOf(result.getProgressPercentage())); cursor.addRow(row); if (DEBUG) Log.d(TAG, "Result[" + (++index) + "]: " + result); } @@ -199,40 +222,87 @@ public class LocalSearchProvider extends ContentProvider { throw new UnsupportedOperationException(); } - /** - * A placeholder to a search result. - */ - public static class SearchResult { - public long channelId; - public String channelNumber; - public String title; - public String description; - public String imageUri; - public String intentAction; - public String intentData; - public String contentType; - public boolean isLive; - public int videoWidth; - public int videoHeight; - public long duration; - public int progressPercentage; - - @Override - public String toString() { - return "SearchResult{channelId=" + channelId + - ", channelNumber=" + channelNumber + - ", title=" + title + - ", description=" + description + - ", imageUri=" + imageUri + - ", intentAction=" + intentAction + - ", intentData=" + intentData + - ", contentType=" + contentType + - ", isLive=" + isLive + - ", videoWidth=" + videoWidth + - ", videoHeight=" + videoHeight + - ", duration=" + duration + - ", progressPercentage=" + progressPercentage + - "}"; + /** A placeholder to a search result. */ + // TODO(b/72052568): Get autovalue to work in aosp master + public abstract static class SearchResult { + public static Builder builder() { + // primitive fields cannot be nullable. Set to default; + return new AutoValue_LocalSearchProvider_SearchResult.Builder() + .setChannelId(0) + .setIsLive(false) + .setVideoWidth(0) + .setVideoHeight(0) + .setDuration(0) + .setProgressPercentage(0); + } + + // TODO(b/72052568): Get autovalue to work in aosp master + abstract static class Builder { + abstract Builder setChannelId(long value); + + abstract Builder setChannelNumber(String value); + + abstract Builder setTitle(String value); + + abstract Builder setDescription(String value); + + abstract Builder setImageUri(String value); + + abstract Builder setIntentAction(String value); + + abstract Builder setIntentData(String value); + + abstract Builder setIntentExtraData(String value); + + abstract Builder setContentType(String value); + + abstract Builder setIsLive(boolean value); + + abstract Builder setVideoWidth(int value); + + abstract Builder setVideoHeight(int value); + + abstract Builder setDuration(long value); + + abstract Builder setProgressPercentage(int value); + + abstract SearchResult build(); } + + abstract long getChannelId(); + + @Nullable + abstract String getChannelNumber(); + + @Nullable + abstract String getTitle(); + + @Nullable + abstract String getDescription(); + + @Nullable + abstract String getImageUri(); + + @Nullable + abstract String getIntentAction(); + + @Nullable + abstract String getIntentData(); + + @Nullable + abstract String getIntentExtraData(); + + @Nullable + abstract String getContentType(); + + abstract boolean getIsLive(); + + abstract int getVideoWidth(); + + abstract int getVideoHeight(); + + abstract long getDuration(); + + abstract int getProgressPercentage(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java index 87eec68e..cb26252b 100644 --- a/src/com/android/tv/search/ProgramGuideSearchFragment.java +++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java @@ -38,12 +38,10 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.PermissionUtils; - +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.util.images.ImageLoader; import java.util.List; public class ProgramGuideSearchFragment extends SearchFragment { @@ -51,44 +49,50 @@ public class ProgramGuideSearchFragment extends SearchFragment { private static final boolean DEBUG = false; private static final int SEARCH_RESULT_MAX = 10; - private final Presenter mPresenter = new Presenter() { - @Override - public Presenter.ViewHolder onCreateViewHolder(ViewGroup viewGroup) { - if (DEBUG) Log.d(TAG, "onCreateViewHolder"); - - ImageCardView cardView = new ImageCardView(mMainActivity); - cardView.setFocusable(true); - cardView.setFocusableInTouchMode(true); - cardView.setMainImageAdjustViewBounds(false); - - Resources res = mMainActivity.getResources(); - cardView.setMainImageDimensions( - res.getDimensionPixelSize(R.dimen.card_image_layout_width), - res.getDimensionPixelSize(R.dimen.card_image_layout_height)); - - return new Presenter.ViewHolder(cardView); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - ImageCardView cardView = (ImageCardView) viewHolder.view; - LocalSearchProvider.SearchResult result = (LocalSearchProvider.SearchResult) o; - if (DEBUG) Log.d(TAG, "onBindViewHolder result:" + result); - - cardView.setTitleText(result.title); - if (!TextUtils.isEmpty(result.imageUri)) { - ImageLoader.loadBitmap(mMainActivity, result.imageUri, mMainCardWidth, - mMainCardHeight, createImageLoaderCallback(cardView)); - } else { - cardView.setMainImage(mMainActivity.getDrawable(R.drawable.ic_launcher)); - } - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - // Do nothing here. - } - }; + private final Presenter mPresenter = + new Presenter() { + @Override + public Presenter.ViewHolder onCreateViewHolder(ViewGroup viewGroup) { + if (DEBUG) Log.d(TAG, "onCreateViewHolder"); + + ImageCardView cardView = new ImageCardView(mMainActivity); + cardView.setFocusable(true); + cardView.setFocusableInTouchMode(true); + cardView.setMainImageAdjustViewBounds(false); + + Resources res = mMainActivity.getResources(); + cardView.setMainImageDimensions( + res.getDimensionPixelSize(R.dimen.card_image_layout_width), + res.getDimensionPixelSize(R.dimen.card_image_layout_height)); + + return new Presenter.ViewHolder(cardView); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + ImageCardView cardView = (ImageCardView) viewHolder.view; + LocalSearchProvider.SearchResult result = (LocalSearchProvider.SearchResult) o; + if (DEBUG) Log.d(TAG, "onBindViewHolder result:" + result); + + cardView.setTitleText(result.getTitle()); + if (!TextUtils.isEmpty(result.getImageUri())) { + ImageLoader.loadBitmap( + mMainActivity, + result.getImageUri(), + mMainCardWidth, + mMainCardHeight, + createImageLoaderCallback(cardView)); + } else { + cardView.setMainImage( + mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96)); + } + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + // Do nothing here. + } + }; private static ImageLoader.ImageLoaderCallback<ImageCardView> createImageLoaderCallback( ImageCardView cardView) { @@ -101,35 +105,42 @@ public class ProgramGuideSearchFragment extends SearchFragment { }; } - private final SearchResultProvider mSearchResultProvider = new SearchResultProvider() { - @Override - public ObjectAdapter getResultsAdapter() { - return mResultAdapter; - } - - @Override - public boolean onQueryTextChange(String query) { - searchAndRefresh(query); - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - searchAndRefresh(query); - return true; - } - }; - - private final OnItemViewClickedListener mItemClickedListener = new OnItemViewClickedListener() { - @Override - public void onItemClicked(Presenter.ViewHolder viewHolder, Object o, RowPresenter - .ViewHolder viewHolder1, Row row) { - LocalSearchProvider.SearchResult result = (LocalSearchProvider.SearchResult) o; - mMainActivity.getFragmentManager().popBackStack(); - mMainActivity.tuneToChannel( - mMainActivity.getChannelDataManager().getChannel(result.channelId)); - } - }; + private final SearchResultProvider mSearchResultProvider = + new SearchResultProvider() { + @Override + public ObjectAdapter getResultsAdapter() { + return mResultAdapter; + } + + @Override + public boolean onQueryTextChange(String query) { + searchAndRefresh(query); + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + searchAndRefresh(query); + return true; + } + }; + + private final OnItemViewClickedListener mItemClickedListener = + new OnItemViewClickedListener() { + @Override + public void onItemClicked( + Presenter.ViewHolder viewHolder, + Object o, + RowPresenter.ViewHolder viewHolder1, + Row row) { + LocalSearchProvider.SearchResult result = (LocalSearchProvider.SearchResult) o; + mMainActivity.getFragmentManager().popBackStack(); + mMainActivity.tuneToChannel( + mMainActivity + .getChannelDataManager() + .getChannel(result.getChannelId())); + } + }; private final ArrayObjectAdapter mResultAdapter = new ArrayObjectAdapter(new ListRowPresenter()); @@ -160,7 +171,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { View v = super.onCreateView(inflater, container, savedInstanceState); v.setBackgroundResource(R.color.program_guide_scrim); - setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_launcher)); + setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96)); setSearchResultProvider(mSearchResultProvider); setOnItemViewClickedListener(mItemClickedListener); return v; @@ -185,8 +196,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { mSearchTask.execute(); } - private class SearchTask extends - AsyncTask<Void, Void, List<LocalSearchProvider.SearchResult>> { + private class SearchTask extends AsyncTask<Void, Void, List<LocalSearchProvider.SearchResult>> { private final String mQuery; public SearchTask(String query) { @@ -195,8 +205,8 @@ public class ProgramGuideSearchFragment extends SearchFragment { @Override protected List<LocalSearchProvider.SearchResult> doInBackground(Void... params) { - return mSearch.search(mQuery, SEARCH_RESULT_MAX, - TvProviderSearch.ACTION_TYPE_AMBIGUOUS); + return mSearch.search( + mQuery, SEARCH_RESULT_MAX, TvProviderSearch.ACTION_TYPE_AMBIGUOUS); } @Override @@ -205,20 +215,23 @@ public class ProgramGuideSearchFragment extends SearchFragment { mResultAdapter.clear(); if (DEBUG) { - Log.d(TAG, "searchAndRefresh query=" + mQuery - + " results=" + ((results == null) ? 0 : results.size())); + Log.d( + TAG, + "searchAndRefresh query=" + + mQuery + + " results=" + + ((results == null) ? 0 : results.size())); } if (results == null || results.size() == 0) { HeaderItem header = - new HeaderItem(0, mMainActivity.getString(R.string - .search_result_no_result)); + new HeaderItem( + 0, mMainActivity.getString(R.string.search_result_no_result)); ArrayObjectAdapter resultsAdapter = new ArrayObjectAdapter(mPresenter); mResultAdapter.add(new ListRow(header, resultsAdapter)); } else { HeaderItem header = - new HeaderItem(0, mMainActivity.getString(R.string - .search_result_title)); + new HeaderItem(0, mMainActivity.getString(R.string.search_result_title)); ArrayObjectAdapter resultsAdapter = new ArrayObjectAdapter(mPresenter); resultsAdapter.addAll(0, results); mResultAdapter.add(new ListRow(header, resultsAdapter)); diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java index d631972a..4866ee84 100644 --- a/src/com/android/tv/search/SearchInterface.java +++ b/src/com/android/tv/search/SearchInterface.java @@ -17,12 +17,9 @@ package com.android.tv.search; import com.android.tv.search.LocalSearchProvider.SearchResult; - import java.util.List; -/** - * Interface for channel and program search. - */ +/** Interface for channel and program search. */ public interface SearchInterface { int ACTION_TYPE_START = 1; int ACTION_TYPE_AMBIGUOUS = 1; @@ -31,11 +28,11 @@ public interface SearchInterface { int ACTION_TYPE_END = 3; /** - * Search channels, inputs, or programs. - * This assumes that parental control settings will not be change while searching. + * Search channels, inputs, or programs. This assumes that parental control settings will not be + * change while searching. * * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, - * or {@link #ACTION_TYPE_AMBIGUOUS}, + * or {@link #ACTION_TYPE_AMBIGUOUS}, */ List<SearchResult> search(String query, int limit, int action); } diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index e7d8a02d..92197f2d 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -32,12 +32,10 @@ import android.os.SystemClock; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; - import com.android.tv.common.TvContentRatingCache; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.search.LocalSearchProvider.SearchResult; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -48,14 +46,15 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; -/** - * An implementation of {@link SearchInterface} to search query from TvProvider directly. - */ +/** An implementation of {@link SearchInterface} to search query from TvProvider directly. */ public class TvProviderSearch implements SearchInterface { private static final String TAG = "TvProviderSearch"; private static final boolean DEBUG = false; + private static final long SEARCH_TIME_FRAME_MS = TimeUnit.DAYS.toMillis(14); + private static final int NO_LIMIT = 0; private final Context mContext; @@ -70,15 +69,16 @@ public class TvProviderSearch implements SearchInterface { } /** - * Search channels, inputs, or programs from TvProvider. - * This assumes that parental control settings will not be change while searching. + * Search channels, inputs, or programs from TvProvider. This assumes that parental control + * settings will not be change while searching. * * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, - * or {@link #ACTION_TYPE_AMBIGUOUS}, + * or {@link #ACTION_TYPE_AMBIGUOUS}, */ @Override @WorkerThread public List<SearchResult> search(String query, int limit, int action) { + // TODO(b/72499463): add a test. List<SearchResult> results = new ArrayList<>(); if (!PermissionUtils.hasAccessAllEpg(mContext)) { // TODO: support this feature for non-system LC app. b/23939816 @@ -107,15 +107,19 @@ public class TvProviderSearch implements SearchInterface { // Lastly, search programs. limit -= results.size(); - results.addAll(searchPrograms(query, null, new String[] { - Programs.COLUMN_TITLE, Programs.COLUMN_SHORT_DESCRIPTION }, - channelsFound, limit)); + results.addAll( + searchPrograms( + query, + null, + new String[] {Programs.COLUMN_TITLE, Programs.COLUMN_SHORT_DESCRIPTION}, + channelsFound, + limit)); } return results; } - private void appendSelectionString(StringBuilder sb, String[] columnForExactMatching, - String[] columnForPartialMatching) { + private void appendSelectionString( + StringBuilder sb, String[] columnForExactMatching, String[] columnForPartialMatching) { boolean firstColumn = true; if (columnForExactMatching != null) { for (String column : columnForExactMatching) { @@ -139,8 +143,12 @@ public class TvProviderSearch implements SearchInterface { } } - private void insertSelectionArgumentStrings(String[] selectionArgs, int pos, - String query, String[] columnForExactMatching, String[] columnForPartialMatching) { + private void insertSelectionArgumentStrings( + String[] selectionArgs, + int pos, + String query, + String[] columnForExactMatching, + String[] columnForPartialMatching) { if (columnForExactMatching != null) { int until = pos + columnForExactMatching.length; for (; pos < until; ++pos) { @@ -162,43 +170,66 @@ public class TvProviderSearch implements SearchInterface { long time = SystemClock.elapsedRealtime(); List<SearchResult> results = new ArrayList<>(); if (TextUtils.isDigitsOnly(query)) { - results.addAll(searchChannels(query, new String[] { Channels.COLUMN_DISPLAY_NUMBER }, - null, channels, NO_LIMIT)); + results.addAll( + searchChannels( + query, + new String[] {Channels.COLUMN_DISPLAY_NUMBER}, + null, + channels, + NO_LIMIT)); if (results.size() > 1) { Collections.sort(results, new ChannelComparatorWithSameDisplayNumber()); } } if (results.size() < limit) { - results.addAll(searchChannels(query, null, - new String[] { Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION }, - channels, limit - results.size())); + results.addAll( + searchChannels( + query, + null, + new String[] { + Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION + }, + channels, + limit - results.size())); } if (results.size() > limit) { results = results.subList(0, limit); } - for (SearchResult result : results) { - fillProgramInfo(result); + for (int i = 0; i < results.size(); i++) { + results.set(i, fillProgramInfo(results.get(i))); } if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " channels. Elapsed time for searching" + - " channels: " + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " channels. Elapsed time for searching" + + " channels: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @WorkerThread - private List<SearchResult> searchChannels(String query, String[] columnForExactMatching, - String[] columnForPartialMatching, Set<Long> channelsFound, int limit) { + private List<SearchResult> searchChannels( + String query, + String[] columnForExactMatching, + String[] columnForPartialMatching, + Set<Long> channelsFound, + int limit) { String[] projection = { - Channels._ID, - Channels.COLUMN_DISPLAY_NUMBER, - Channels.COLUMN_DISPLAY_NAME, - Channels.COLUMN_DESCRIPTION + Channels._ID, + Channels.COLUMN_DISPLAY_NUMBER, + Channels.COLUMN_DISPLAY_NAME, + Channels.COLUMN_DESCRIPTION }; StringBuilder sb = new StringBuilder(); - sb.append(Channels.COLUMN_BROWSABLE).append("=1 AND ") - .append(Channels.COLUMN_SEARCHABLE).append("=1"); + sb.append(Channels.COLUMN_BROWSABLE) + .append("=1 AND ") + .append(Channels.COLUMN_SEARCHABLE) + .append("=1"); if (mTvInputManager.isParentalControlsEnabled()) { sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0"); } @@ -207,16 +238,18 @@ public class TvProviderSearch implements SearchInterface { sb.append(")"); String selection = sb.toString(); - int len = (columnForExactMatching == null ? 0 : columnForExactMatching.length) + - (columnForPartialMatching == null ? 0 : columnForPartialMatching.length); + int len = + (columnForExactMatching == null ? 0 : columnForExactMatching.length) + + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length); String[] selectionArgs = new String[len]; - insertSelectionArgumentStrings(selectionArgs, 0, query, columnForExactMatching, - columnForPartialMatching); + insertSelectionArgumentStrings( + selectionArgs, 0, query, columnForExactMatching, columnForPartialMatching); List<SearchResult> searchResults = new ArrayList<>(); - try (Cursor c = mContentResolver.query(Channels.CONTENT_URI, projection, selection, - selectionArgs, null)) { + try (Cursor c = + mContentResolver.query( + Channels.CONTENT_URI, projection, selection, selectionArgs, null)) { if (c != null) { int count = 0; while (c.moveToNext()) { @@ -227,19 +260,19 @@ public class TvProviderSearch implements SearchInterface { } channelsFound.add(id); - SearchResult result = new SearchResult(); - result.channelId = id; - result.channelNumber = c.getString(1); - result.title = c.getString(2); - result.description = c.getString(3); - result.imageUri = TvContract.buildChannelLogoUri(result.channelId).toString(); - result.intentAction = Intent.ACTION_VIEW; - result.intentData = buildIntentData(result.channelId); - result.contentType = Programs.CONTENT_ITEM_TYPE; - result.isLive = true; - result.progressPercentage = LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + SearchResult.Builder result = SearchResult.builder(); + result.setChannelId(id); + result.setChannelNumber(c.getString(1)); + result.setTitle(c.getString(2)); + result.setDescription(c.getString(3)); + result.setImageUri(TvContract.buildChannelLogoUri(id).toString()); + result.setIntentAction(Intent.ACTION_VIEW); + result.setIntentData(buildIntentData(id)); + result.setContentType(Programs.CONTENT_ITEM_TYPE); + result.setIsLive(true); + result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE); - searchResults.add(result); + searchResults.add(result.build()); if (limit != NO_LIMIT && ++count >= limit) { break; @@ -256,43 +289,55 @@ public class TvProviderSearch implements SearchInterface { * blocked. */ @WorkerThread - private void fillProgramInfo(SearchResult result) { + private SearchResult fillProgramInfo(SearchResult result) { long now = System.currentTimeMillis(); - Uri uri = TvContract.buildProgramsUriForChannel(result.channelId, now, now); - String[] projection = new String[] { - Programs.COLUMN_TITLE, - Programs.COLUMN_POSTER_ART_URI, - Programs.COLUMN_CONTENT_RATING, - Programs.COLUMN_VIDEO_WIDTH, - Programs.COLUMN_VIDEO_HEIGHT, - Programs.COLUMN_START_TIME_UTC_MILLIS, - Programs.COLUMN_END_TIME_UTC_MILLIS - }; + Uri uri = TvContract.buildProgramsUriForChannel(result.getChannelId(), now, now); + String[] projection = + new String[] { + Programs.COLUMN_TITLE, + Programs.COLUMN_POSTER_ART_URI, + Programs.COLUMN_CONTENT_RATING, + Programs.COLUMN_VIDEO_WIDTH, + Programs.COLUMN_VIDEO_HEIGHT, + Programs.COLUMN_START_TIME_UTC_MILLIS, + Programs.COLUMN_END_TIME_UTC_MILLIS + }; try (Cursor c = mContentResolver.query(uri, projection, null, null, null)) { if (c != null && c.moveToNext() && !isRatingBlocked(c.getString(2))) { - String channelName = result.title; + String channelName = result.getTitle(); + String channelNumber = result.getChannelNumber(); + SearchResult.Builder builder = SearchResult.builder(); long startUtcMillis = c.getLong(5); long endUtcMillis = c.getLong(6); - result.title = c.getString(0); - result.description = buildProgramDescription(result.channelNumber, channelName, - startUtcMillis, endUtcMillis); + builder.setTitle(c.getString(0)); + builder.setDescription( + buildProgramDescription( + channelNumber, channelName, startUtcMillis, endUtcMillis)); String imageUri = c.getString(1); if (imageUri != null) { - result.imageUri = imageUri; + builder.setImageUri(imageUri); } - result.videoWidth = c.getInt(3); - result.videoHeight = c.getInt(4); - result.duration = endUtcMillis - startUtcMillis; - result.progressPercentage = getProgressPercentage(startUtcMillis, endUtcMillis); + builder.setVideoWidth(c.getInt(3)); + builder.setVideoHeight(c.getInt(4)); + builder.setDuration(endUtcMillis - startUtcMillis); + builder.setProgressPercentage(getProgressPercentage(startUtcMillis, endUtcMillis)); + return builder.build(); } } + return result; } - private String buildProgramDescription(String channelNumber, String channelName, - long programStartUtcMillis, long programEndUtcMillis) { + private String buildProgramDescription( + String channelNumber, + String channelName, + long programStartUtcMillis, + long programEndUtcMillis) { return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false) - + System.lineSeparator() + channelNumber + " " + channelName; + + System.lineSeparator() + + channelNumber + + " " + + channelName; } private int getProgressPercentage(long startUtcMillis, long endUtcMillis) { @@ -300,23 +345,28 @@ public class TvProviderSearch implements SearchInterface { if (startUtcMillis > current || endUtcMillis <= current) { return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; } - return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); + return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); } @WorkerThread - private List<SearchResult> searchPrograms(String query, String[] columnForExactMatching, - String[] columnForPartialMatching, Set<Long> channelsFound, int limit) { + private List<SearchResult> searchPrograms( + String query, + String[] columnForExactMatching, + String[] columnForPartialMatching, + Set<Long> channelsFound, + int limit) { if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'"); long time = SystemClock.elapsedRealtime(); String[] projection = { - Programs.COLUMN_CHANNEL_ID, - Programs.COLUMN_TITLE, - Programs.COLUMN_POSTER_ART_URI, - Programs.COLUMN_CONTENT_RATING, - Programs.COLUMN_VIDEO_WIDTH, - Programs.COLUMN_VIDEO_HEIGHT, - Programs.COLUMN_START_TIME_UTC_MILLIS, - Programs.COLUMN_END_TIME_UTC_MILLIS + Programs.COLUMN_CHANNEL_ID, + Programs.COLUMN_TITLE, + Programs.COLUMN_POSTER_ART_URI, + Programs.COLUMN_CONTENT_RATING, + Programs.COLUMN_VIDEO_WIDTH, + Programs.COLUMN_VIDEO_HEIGHT, + Programs.COLUMN_START_TIME_UTC_MILLIS, + Programs.COLUMN_END_TIME_UTC_MILLIS, + Programs._ID }; StringBuilder sb = new StringBuilder(); @@ -327,17 +377,21 @@ public class TvProviderSearch implements SearchInterface { sb.append(")"); String selection = sb.toString(); - int len = (columnForExactMatching == null ? 0 : columnForExactMatching.length) + - (columnForPartialMatching == null ? 0 : columnForPartialMatching.length); + int len = + (columnForExactMatching == null ? 0 : columnForExactMatching.length) + + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length); String[] selectionArgs = new String[len + 2]; - selectionArgs[0] = selectionArgs[1] = String.valueOf(System.currentTimeMillis()); - insertSelectionArgumentStrings(selectionArgs, 2, query, columnForExactMatching, - columnForPartialMatching); + long now = System.currentTimeMillis(); + selectionArgs[0] = String.valueOf(now + SEARCH_TIME_FRAME_MS); + selectionArgs[1] = String.valueOf(now); + insertSelectionArgumentStrings( + selectionArgs, 2, query, columnForExactMatching, columnForPartialMatching); List<SearchResult> searchResults = new ArrayList<>(); - try (Cursor c = mContentResolver.query(Programs.CONTENT_URI, projection, selection, - selectionArgs, null)) { + try (Cursor c = + mContentResolver.query( + Programs.CONTENT_URI, projection, selection, selectionArgs, null)) { if (c != null) { int count = 0; while (c.moveToNext()) { @@ -350,41 +404,53 @@ public class TvProviderSearch implements SearchInterface { // Don't know whether the channel is searchable or not. String[] channelProjection = { - Channels._ID, - Channels.COLUMN_DISPLAY_NUMBER, - Channels.COLUMN_DISPLAY_NAME + Channels._ID, Channels.COLUMN_DISPLAY_NUMBER, Channels.COLUMN_DISPLAY_NAME }; sb = new StringBuilder(); - sb.append(Channels._ID).append("=? AND ") - .append(Channels.COLUMN_BROWSABLE).append("=1 AND ") - .append(Channels.COLUMN_SEARCHABLE).append("=1"); + sb.append(Channels._ID) + .append("=? AND ") + .append(Channels.COLUMN_BROWSABLE) + .append("=1 AND ") + .append(Channels.COLUMN_SEARCHABLE) + .append("=1"); if (mTvInputManager.isParentalControlsEnabled()) { sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0"); } String selectionChannel = sb.toString(); - try (Cursor cChannel = mContentResolver.query(Channels.CONTENT_URI, - channelProjection, selectionChannel, - new String[] { String.valueOf(id) }, null)) { - if (cChannel != null && cChannel.moveToNext() + try (Cursor cChannel = + mContentResolver.query( + Channels.CONTENT_URI, + channelProjection, + selectionChannel, + new String[] {String.valueOf(id)}, + null)) { + if (cChannel != null + && cChannel.moveToNext() && !isRatingBlocked(c.getString(3))) { long startUtcMillis = c.getLong(6); long endUtcMillis = c.getLong(7); - SearchResult result = new SearchResult(); - result.channelId = c.getLong(0); - result.title = c.getString(1); - result.description = buildProgramDescription(cChannel.getString(1), - cChannel.getString(2), startUtcMillis, endUtcMillis); - result.imageUri = c.getString(2); - result.intentAction = Intent.ACTION_VIEW; - result.intentData = buildIntentData(id); - result.contentType = Programs.CONTENT_ITEM_TYPE; - result.isLive = true; - result.videoWidth = c.getInt(4); - result.videoHeight = c.getInt(5); - result.duration = endUtcMillis - startUtcMillis; - result.progressPercentage = getProgressPercentage(startUtcMillis, - endUtcMillis); - searchResults.add(result); + SearchResult.Builder result = SearchResult.builder(); + result.setChannelId(c.getLong(0)); + result.setTitle(c.getString(1)); + result.setDescription( + buildProgramDescription( + cChannel.getString(1), + cChannel.getString(2), + startUtcMillis, + endUtcMillis)); + result.setImageUri(c.getString(2)); + result.setIntentAction(Intent.ACTION_VIEW); + result.setIntentData(buildIntentData(id)); + result.setIntentExtraData( + TvContract.buildProgramUri(c.getLong(8)).toString()); + result.setContentType(Programs.CONTENT_ITEM_TYPE); + result.setIsLive(true); + result.setVideoWidth(c.getInt(4)); + result.setVideoHeight(c.getInt(5)); + result.setDuration(endUtcMillis - startUtcMillis); + result.setProgressPercentage( + getProgressPercentage(startUtcMillis, endUtcMillis)); + searchResults.add(result.build()); if (limit != NO_LIMIT && ++count >= limit) { break; @@ -395,8 +461,14 @@ public class TvProviderSearch implements SearchInterface { } } if (DEBUG) { - Log.d(TAG, "Found " + searchResults.size() + " programs. Elapsed time for searching" + - " programs: " + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + searchResults.size() + + " programs. Elapsed time for searching" + + " programs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return searchResults; } @@ -439,9 +511,14 @@ public class TvProviderSearch implements SearchInterface { results.add(buildSearchResultForInput(input.getId())); if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " inputs. Elapsed time for" + - " searching inputs: " + (SystemClock.elapsedRealtime() - time) + - "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " inputs. Elapsed time for" + + " searching inputs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @@ -455,22 +532,33 @@ public class TvProviderSearch implements SearchInterface { } String label = canonicalizeLabel(input.loadLabel(mContext)); String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext)); - if ((label != null && label.contains(query)) || - (customLabel != null && customLabel.contains(query))) { + if ((label != null && label.contains(query)) + || (customLabel != null && customLabel.contains(query))) { results.add(buildSearchResultForInput(input.getId())); if (results.size() >= limit) { if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " inputs. Elapsed time for" + - " searching inputs: " + (SystemClock.elapsedRealtime() - time) + - "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " inputs. Elapsed time for" + + " searching inputs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } } } if (DEBUG) { - Log.d(TAG, "Found " + results.size() + " inputs. Elapsed time for searching" + - " inputs: " + (SystemClock.elapsedRealtime() - time) + "(msec)"); + Log.d( + TAG, + "Found " + + results.size() + + " inputs. Elapsed time for searching" + + " inputs: " + + (SystemClock.elapsedRealtime() - time) + + "(msec)"); } return results; } @@ -481,10 +569,10 @@ public class TvProviderSearch implements SearchInterface { } private SearchResult buildSearchResultForInput(String inputId) { - SearchResult result = new SearchResult(); - result.intentAction = Intent.ACTION_VIEW; - result.intentData = TvContract.buildChannelUriForPassthroughInput(inputId).toString(); - return result; + SearchResult.Builder result = SearchResult.builder(); + result.setIntentAction(Intent.ACTION_VIEW); + result.setIntentData(TvContract.buildChannelUriForPassthroughInput(inputId).toString()); + return result.build(); } @WorkerThread @@ -494,33 +582,35 @@ public class TvProviderSearch implements SearchInterface { @Override public int compare(SearchResult lhs, SearchResult rhs) { // Show recently watched channel first - Long lhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(lhs.channelId); + Long lhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(lhs.getChannelId()); if (lhsMaxWatchStartTime == null) { - lhsMaxWatchStartTime = getMaxWatchStartTime(lhs.channelId); - mMaxWatchStartTimeMap.put(lhs.channelId, lhsMaxWatchStartTime); + lhsMaxWatchStartTime = getMaxWatchStartTime(lhs.getChannelId()); + mMaxWatchStartTimeMap.put(lhs.getChannelId(), lhsMaxWatchStartTime); } - Long rhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(rhs.channelId); + Long rhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(rhs.getChannelId()); if (rhsMaxWatchStartTime == null) { - rhsMaxWatchStartTime = getMaxWatchStartTime(rhs.channelId); - mMaxWatchStartTimeMap.put(rhs.channelId, rhsMaxWatchStartTime); + rhsMaxWatchStartTime = getMaxWatchStartTime(rhs.getChannelId()); + mMaxWatchStartTimeMap.put(rhs.getChannelId(), rhsMaxWatchStartTime); } if (!Objects.equals(lhsMaxWatchStartTime, rhsMaxWatchStartTime)) { return Long.compare(rhsMaxWatchStartTime, lhsMaxWatchStartTime); } // Show recently added channel first if there's no watch history. - return Long.compare(rhs.channelId, lhs.channelId); + return Long.compare(rhs.getChannelId(), lhs.getChannelId()); } private long getMaxWatchStartTime(long channelId) { Uri uri = WatchedPrograms.CONTENT_URI; - String[] projections = new String[] { - "MAX(" + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS - + ") AS max_watch_start_time" - }; + String[] projections = + new String[] { + "MAX(" + + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + + ") AS max_watch_start_time" + }; String selection = WatchedPrograms.COLUMN_CHANNEL_ID + "=?"; - String[] selectionArgs = new String[] { Long.toString(channelId) }; - try (Cursor c = mContentResolver.query(uri, projections, selection, selectionArgs, - null)) { + String[] selectionArgs = new String[] {Long.toString(channelId)}; + try (Cursor c = + mContentResolver.query(uri, projections, selection, selectionArgs, null)) { if (c != null && c.moveToNext()) { return c.getLong(0); } diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java index 7e627410..c6b10e52 100644 --- a/src/com/android/tv/setup/SystemSetupActivity.java +++ b/src/com/android/tv/setup/SystemSetupActivity.java @@ -24,27 +24,22 @@ import android.content.Intent; import android.media.tv.TvInputInfo; import android.os.Bundle; import android.widget.Toast; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.SetupPassthroughActivity; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.TvSingletons; +import com.android.tv.common.CommonConstants; import com.android.tv.common.ui.setup.SetupActivity; -import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.TvApplication; -import com.android.tv.data.ChannelDataManager; +import com.android.tv.common.util.CommonUtils; import com.android.tv.onboarding.SetupSourcesFragment; import com.android.tv.util.OnboardingUtils; import com.android.tv.util.SetupUtils; import com.android.tv.util.TvInputManagerHelper; -/** - * A activity to start input sources setup fragment for initial setup flow. - */ +/** A activity to start input sources setup fragment for initial setup flow. */ public class SystemSetupActivity extends SetupActivity { private static final String SYSTEM_SETUP = - "com.android.tv.action.LAUNCH_SYSTEM_SETUP"; + CommonConstants.BASE_PACKAGE + ".action.LAUNCH_SYSTEM_SETUP"; private static final int SHOW_RIPPLE_DURATION_MS = 266; private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; @@ -58,7 +53,7 @@ public class SystemSetupActivity extends SetupActivity { finish(); return; } - ApplicationSingletons singletons = TvApplication.getSingletons(this); + TvSingletons singletons = TvSingletons.getSingletons(this); mInputManager = singletons.getTvInputManagerHelper(); } @@ -68,12 +63,14 @@ public class SystemSetupActivity extends SetupActivity { } private void showMerchantCollection() { - executeActionWithDelay(new Runnable() { - @Override - public void run() { - startActivity(OnboardingUtils.ONLINE_STORE_INTENT); - } - }, SHOW_RIPPLE_DURATION_MS); + executeActionWithDelay( + new Runnable() { + @Override + public void run() { + startActivity(OnboardingUtils.ONLINE_STORE_INTENT); + } + }, + SHOW_RIPPLE_DURATION_MS); } @Override @@ -84,38 +81,50 @@ public class SystemSetupActivity extends SetupActivity { case SetupSourcesFragment.ACTION_ONLINE_STORE: showMerchantCollection(); return true; - case SetupSourcesFragment.ACTION_SETUP_INPUT: { - String inputId = params.getString( - SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = TvCommonUtils.createSetupIntent(input); - if (intent == null) { - Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) - .show(); + case SetupSourcesFragment.ACTION_SETUP_INPUT: + { + String inputId = + params.getString( + SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + Intent intent = CommonUtils.createSetupIntent(input); + if (intent == null) { + Toast.makeText( + this, + R.string.msg_no_setup_activity, + Toast.LENGTH_SHORT) + .show(); + return true; + } + // Even though other app can handle the intent, the setup launched by + // Live + // channels should go through Live channels SetupPassthroughActivity. + intent.setComponent( + new ComponentName(this, SetupPassthroughActivity.class)); + try { + // Now we know that the user intends to set up this input. Grant + // permission for writing EPG data. + SetupUtils.grantEpgPermission( + this, input.getServiceInfo().packageName); + startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); + } catch (ActivityNotFoundException e) { + Toast.makeText( + this, + getString( + R.string.msg_unable_to_start_setup_activity, + input.loadLabel(this)), + Toast.LENGTH_SHORT) + .show(); + } return true; } - // Even though other app can handle the intent, the setup launched by Live - // channels should go through Live channels SetupPassthroughActivity. - intent.setComponent(new ComponentName(this, - SetupPassthroughActivity.class)); - try { - // Now we know that the user intends to set up this input. Grant - // permission for writing EPG data. - SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName); - startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, - getString(R.string.msg_unable_to_start_setup_activity, - input.loadLabel(this)), Toast.LENGTH_SHORT).show(); + case SetupMultiPaneFragment.ACTION_DONE: + { + // To make sure user can finish setup flow, set result as RESULT_OK. + setResult(Activity.RESULT_OK); + finish(); + return true; } - return true; - } - case SetupMultiPaneFragment.ACTION_DONE: { - // To make sure user can finish setup flow, set result as RESULT_OK. - setResult(Activity.RESULT_OK); - finish(); - return true; - } } break; } diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java deleted file mode 100644 index a255de3e..00000000 --- a/src/com/android/tv/tuner/ChannelScanFileParser.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.util.Log; - -import com.android.tv.tuner.data.nano.Channel; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; - -/** - * Parses plain text formatted scan files, which contain the list of channels. - */ -public 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.TYPE_TUNER, frequency, modulation, null, - radioFrequencyNumber); - } - - public static ScanChannel forFile(int frequency, String filename) { - return new ScanChannel(Channel.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. - * - * @param is {@link InputStream} of a scan file. Each line matches one channel. - * The line format of the scan file is as follows:<br> - * "A <frequency> <modulation>". - * @return a list of {@link ScanChannel} objects parsed - */ - public static List<ScanChannel> parseScanFile(InputStream is) { - BufferedReader in = new BufferedReader(new InputStreamReader(is)); - String line; - List<ScanChannel> scanChannelList = new ArrayList<>(); - try { - while ((line = in.readLine()) != null) { - if (line.isEmpty()) { - continue; - } - if (line.charAt(0) == '#') { - // Skip comment line - continue; - } - String[] tokens = line.split("\\s+"); - if (tokens.length != 3 && tokens.length != 4) { - continue; - } - scanChannelList.add(ScanChannel.forTuner(Integer.parseInt(tokens[1]), tokens[2], - tokens.length == 4 ? Integer.parseInt(tokens[3]) : null)); - } - } catch (IOException e) { - Log.e(TAG, "error on parseScanFile()", e); - } - return scanChannelList; - } -} diff --git a/src/com/android/tv/tuner/DvbDeviceAccessor.java b/src/com/android/tv/tuner/DvbDeviceAccessor.java deleted file mode 100644 index 4f5d8ee4..00000000 --- a/src/com/android/tv/tuner/DvbDeviceAccessor.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.content.Context; -import android.media.tv.TvInputManager; -import android.os.ParcelFileDescriptor; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.android.tv.common.recording.RecordingCapability; -import com.android.tv.tuner.tvinput.TunerTvInputService; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -/** - * Provides with the file descriptors to access DVB device. - */ -public class DvbDeviceAccessor { - private static final String TAG = "DvbDeviceAccessor"; - - @IntDef({DVB_DEVICE_DEMUX, DVB_DEVICE_DVR, DVB_DEVICE_FRONTEND}) - @Retention(RetentionPolicy.SOURCE) - public @interface DvbDevice {} - public static final int DVB_DEVICE_DEMUX = 0; // TvInputManager.DVB_DEVICE_DEMUX; - public static final int DVB_DEVICE_DVR = 1; // TvInputManager.DVB_DEVICE_DVR; - public static final int DVB_DEVICE_FRONTEND = 2; // TvInputManager.DVB_DEVICE_FRONTEND; - - private static Method sGetDvbDeviceListMethod; - private static Method sOpenDvbDeviceMethod; - - private final TvInputManager mTvInputManager; - - static { - try { - Class tvInputManagerClass = Class.forName("android.media.tv.TvInputManager"); - Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo"); - sGetDvbDeviceListMethod = tvInputManagerClass.getDeclaredMethod("getDvbDeviceList"); - sGetDvbDeviceListMethod.setAccessible(true); - sOpenDvbDeviceMethod = tvInputManagerClass.getDeclaredMethod( - "openDvbDevice", dvbDeviceInfoClass, Integer.TYPE); - sOpenDvbDeviceMethod.setAccessible(true); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Couldn't find class", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "Couldn't find method", e); - } - } - - public DvbDeviceAccessor(Context context) { - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - } - - public List<DvbDeviceInfoWrapper> getDvbDeviceList() { - try { - List<DvbDeviceInfoWrapper> wrapperList = new ArrayList<>(); - List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager); - for (Object dvbDeviceInfo : dvbDeviceInfoList) { - wrapperList.add(new DvbDeviceInfoWrapper(dvbDeviceInfo)); - } - Collections.sort(wrapperList); - return wrapperList; - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return null; - } - - /** - * Returns the number of currently connected DVB devices. - */ - public int getNumOfDvbDevices() { - List<DvbDeviceInfoWrapper> dvbDeviceList = getDvbDeviceList(); - return dvbDeviceList == null ? 0 : dvbDeviceList.size(); - } - - public boolean isDvbDeviceAvailable() { - try { - List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager); - return (!dvbDeviceInfoList.isEmpty()); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return false; - } - - public ParcelFileDescriptor openDvbDevice(DvbDeviceInfoWrapper deviceInfo, - @DvbDevice int device) { - try { - return (ParcelFileDescriptor) sOpenDvbDeviceMethod.invoke( - mTvInputManager, deviceInfo.getDvbDeviceInfo(), device); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return null; - } - - /** - * Returns the current recording capability for USB tuner. - * @param inputId the input id to use. - */ - public RecordingCapability getRecordingCapability(String inputId) { - List<DvbDeviceInfoWrapper> deviceList = getDvbDeviceList(); - // TODO(DVR) implement accurate capabilities and updating values when needed. - return RecordingCapability.builder() - .setInputId(inputId) - .setMaxConcurrentPlayingSessions(1) - .setMaxConcurrentTunedSessions(deviceList.size()) - .setMaxConcurrentSessionsOfAllTypes(deviceList.size() + 1) - .build(); - } - - public static class DvbDeviceInfoWrapper implements Comparable<DvbDeviceInfoWrapper> { - private static Method sGetAdapterIdMethod; - private static Method sGetDeviceIdMethod; - private final Object mDvbDeviceInfo; - private final int mAdapterId; - private final int mDeviceId; - private final long mId; - - static { - try { - Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo"); - sGetAdapterIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getAdapterId"); - sGetAdapterIdMethod.setAccessible(true); - sGetDeviceIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getDeviceId"); - sGetDeviceIdMethod.setAccessible(true); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Couldn't find class", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "Couldn't find method", e); - } - } - - public DvbDeviceInfoWrapper(Object dvbDeviceInfo) { - mDvbDeviceInfo = dvbDeviceInfo; - mAdapterId = initAdapterId(); - mDeviceId = initDeviceId(); - mId = (((long) getAdapterId()) << 32) | (getDeviceId() & 0xffffffffL); - } - - public long getId() { - return mId; - } - - public int getAdapterId() { - return mAdapterId; - } - - private int initAdapterId() { - try { - return (int) sGetAdapterIdMethod.invoke(mDvbDeviceInfo); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } - return -1; - } - - public int getDeviceId() { - return mDeviceId; - } - - private int initDeviceId() { - try { - return (int) sGetDeviceIdMethod.invoke(mDvbDeviceInfo); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } - return -1; - } - - public Object getDvbDeviceInfo() { - return mDvbDeviceInfo; - } - - @Override - public int compareTo(@NonNull DvbDeviceInfoWrapper another) { - if (getAdapterId() != another.getAdapterId()) { - return getAdapterId() - another.getAdapterId(); - } - return getDeviceId() - another.getDeviceId(); - } - - @Override - public String toString() { - return String.format(Locale.US, "DvbDeviceInfo {adapterId: %d, deviceId: %d}", - getAdapterId(), - getDeviceId()); - } - } -} diff --git a/src/com/android/tv/tuner/DvbTunerHal.java b/src/com/android/tv/tuner/DvbTunerHal.java deleted file mode 100644 index ea977230..00000000 --- a/src/com/android/tv/tuner/DvbTunerHal.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.content.Context; -import android.os.ParcelFileDescriptor; -import android.util.Log; -import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper; - -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - -/** - * A class to handle a hardware Linux DVB API supported tuner device. - */ -public class DvbTunerHal extends TunerHal { - - private static final Object sLock = new Object(); - // @GuardedBy("sLock") - private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>(); - - private final DvbDeviceAccessor mDvbDeviceAccessor; - private DvbDeviceInfoWrapper mDvbDeviceInfo; - - protected DvbTunerHal(Context context) { - super(context); - mDvbDeviceAccessor = new DvbDeviceAccessor(context); - } - - @Override - protected boolean openFirstAvailable() { - List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList(); - if (deviceInfoList == null || deviceInfoList.isEmpty()) { - Log.e(TAG, "There's no dvb device attached"); - return false; - } - synchronized (sLock) { - for (DvbDeviceInfoWrapper deviceInfo : deviceInfoList) { - if (!sUsedDvbDevices.contains(deviceInfo)) { - if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo); - mDvbDeviceInfo = deviceInfo; - sUsedDvbDevices.add(deviceInfo); - getDeliverySystemTypeFromDevice(); - return true; - } - } - } - Log.e(TAG, "There's no available dvb devices"); - return false; - } - - /** - * Acquires the tuner device. The requested device will be locked to the current instance if - * it's not acquired by others. - * - * @param deviceInfo a tuner device to open - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - protected boolean open(DvbDeviceInfoWrapper deviceInfo) { - if (deviceInfo == null) { - Log.e(TAG, "Device info should not be null"); - return false; - } - if (mDvbDeviceInfo != null) { - Log.e(TAG, "Already acquired"); - return false; - } - List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList(); - if (deviceInfoList == null || deviceInfoList.isEmpty()) { - Log.e(TAG, "There's no dvb device attached"); - return false; - } - for (DvbDeviceInfoWrapper deviceInfoWrapper : deviceInfoList) { - if (deviceInfoWrapper.compareTo(deviceInfo) == 0) { - synchronized (sLock) { - if (sUsedDvbDevices.contains(deviceInfo)) { - Log.e(TAG, deviceInfo + " is already taken"); - return false; - } - sUsedDvbDevices.add(deviceInfo); - } - if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo); - mDvbDeviceInfo = deviceInfo; - return true; - } - } - Log.e(TAG, "There's no such dvb device attached"); - return false; - } - - @Override - public void close() { - if (mDvbDeviceInfo != null) { - if (isStreaming()) { - stopTune(); - } - nativeFinalize(mDvbDeviceInfo.getId()); - synchronized (sLock) { - sUsedDvbDevices.remove(mDvbDeviceInfo); - } - mDvbDeviceInfo = null; - } - } - - @Override - protected boolean isDeviceOpen() { - return (mDvbDeviceInfo != null); - } - - @Override - protected long getDeviceId() { - if (mDvbDeviceInfo != null) { - return mDvbDeviceInfo.getId(); - } - return -1; - } - - @Override - protected int openDvbFrontEndFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_FRONTEND); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - @Override - protected int openDvbDemuxFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DEMUX); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - @Override - protected int openDvbDvrFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DVR); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - /** - * Gets the number of USB tuner devices currently present. - */ - public static int getNumberOfDevices(Context context) { - try { - return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); - } catch (Exception e) { - return 0; - } - } -} diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java deleted file mode 100644 index 1176cdf0..00000000 --- a/src/com/android/tv/tuner/TunerHal.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.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.Features; -import com.android.tv.customization.TvCustomizationManager; - -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 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; - - @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; - private boolean mIsStreaming; - private int mFrequency; - private String mModulation; - - static { - System.loadLibrary("tunertvinput_jni"); - } - - /** - * Creates a TunerHal instance. - * @param context context for creating the TunerHal instance - * @return the TunerHal instance - */ - @WorkerThread - public synchronized static 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. - */ - static boolean useBuiltInTuner(Context context) { - return getBuiltInTunerType(context) != 0; - } - - private static @BuiltInTunerType int getBuiltInTunerType(Context context) { - if (sBuiltInTunerType == null) { - sBuiltInTunerType = 0; - if (TvCustomizationManager.hasLinuxDvbBuiltInTuner(context) - && DvbTunerHal.getNumberOfDevices(context) > 0) { - sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB; - } - } - return sBuiltInTunerType; - } - - protected TunerHal(Context context) { - mIsStreaming = false; - mFrequency = -1; - mModulation = null; - } - - protected boolean isStreaming() { - return mIsStreaming; - } - - protected void getDeliverySystemTypeFromDevice() { - if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) { - mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId()); - } - } - - /** - * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels - * of the same frequency. - */ - public boolean isReusable() { - return true; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - close(); - } - - 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 - * @param modulation a modulation method of the channel to tune to - * @param channelNumber channel number when channel number is already known. Some tuner HAL - * may use channelNumber instead of frequency for tune. - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - public synchronized boolean tune(int frequency, @ModulationType String modulation, - String channelNumber) { - if (!isDeviceOpen()) { - Log.e(TAG, "There's no available device"); - return false; - } - if (mIsStreaming) { - nativeCloseAllPidFilters(getDeviceId()); - mIsStreaming = false; - } - - // When tuning to a new channel in the same frequency, there's no need to stop current tuner - // device completely and the only thing necessary for tuning is reopening pid filters. - if (mFrequency == frequency && Objects.equals(mModulation, modulation)) { - addPidFilter(PID_PAT, FILTER_TYPE_OTHER); - addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { - addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); - addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); - } - mIsStreaming = true; - return true; - } - int timeout_ms = modulation.equals(MODULATION_8VSB) ? DEFAULT_VSB_TUNE_TIMEOUT_MS - : DEFAULT_QAM_TUNE_TIMEOUT_MS; - if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) { - addPidFilter(PID_PAT, FILTER_TYPE_OTHER); - addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { - addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); - addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); - } - mFrequency = frequency; - mModulation = modulation; - mIsStreaming = true; - return true; - } - return false; - } - - protected native boolean nativeTune(long deviceId, int frequency, - @ModulationType String modulation, int timeout_ms); - - /** - * Sets a pid filter. This should be set after setting a channel. - * - * @param pid a pid number to be added to filter list - * @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX) - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - public synchronized boolean addPidFilter(int pid, @FilterType int filterType) { - if (!isDeviceOpen()) { - Log.e(TAG, "There's no available device"); - return false; - } - if (pid >= 0 && pid <= 0x1fff) { - nativeAddPidFilter(getDeviceId(), pid, filterType); - return true; - } - return false; - } - - protected native void nativeAddPidFilter(long deviceId, int pid, @FilterType int filterType); - protected native void nativeCloseAllPidFilters(long deviceId); - protected native void nativeSetHasPendingTune(long deviceId, boolean hasPendingTune); - protected native int nativeGetDeliverySystemType(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. - */ - public synchronized void stopTune() { - if (isDeviceOpen()) { - if (mIsStreaming) { - nativeCloseAllPidFilters(getDeviceId()); - } - nativeStopTune(getDeviceId()); - } - mIsStreaming = false; - mFrequency = -1; - mModulation = null; - } - - public void setHasPendingTune(boolean hasPendingTune) { - nativeSetHasPendingTune(getDeviceId(), hasPendingTune); - } - - public int getDeliverySystemType() { - return mDeliverySystemType; - } - - 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. - * - * @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 - * should be equal to the length of the buffer. - * @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. - */ - public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) { - if (isDeviceOpen()) { - return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize); - } else { - return 0; - } - } - - protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize); - - /** - * Opens Linux DVB frontend device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbFrontEndFd() { - return -1; - } - - /** - * Opens Linux DVB demux device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbDemuxFd() { - return -1; - } - - /** - * Opens Linux DVB dvr device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbDvrFd() { - return -1; - } -} diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java index e06b9b4a..02611bbf 100644 --- a/src/com/android/tv/tuner/TunerInputController.java +++ b/src/com/android/tv/tuner/TunerInputController.java @@ -17,6 +17,9 @@ package com.android.tv.tuner; import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -24,10 +27,14 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; @@ -38,118 +45,132 @@ import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; - -import com.android.tv.Features; import com.android.tv.R; +import com.android.tv.Starter; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.setup.TunerSetupActivity; -import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.SystemPropertiesProxy; -import com.android.tv.tuner.util.TunerInputInfoUtils; +import com.android.tv.TvSingletons; +import com.android.tv.common.BuildConfig; +import com.android.tv.common.util.SystemPropertiesProxy; + +import com.android.tv.tuner.setup.BaseTunerSetupActivity; +import com.android.tv.tuner.util.TunerInputInfoUtils; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** - * Controls the package visibility of {@link TunerTvInputService}. - * <p> - * Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, - * {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} - * to update the connection status of the supported USB TV tuners. + * Controls the package visibility of {@link BaseTunerTvInputService}. + * + * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code + * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to + * update the connection status of the supported USB TV tuners. */ public class TunerInputController { - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private static final String TAG = "TunerInputController"; private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner"; private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch"; private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd"; + private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s"; - /** - * Action of {@link Intent} to check network connection repeatedly when it is necessary. - */ - private static final String CHECKING_NETWORK_CONNECTION = - "com.android.tv.action.CHECKING_NETWORK_CONNECTION"; + /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */ + private static final String CHECKING_NETWORK_TUNER_STATUS = + "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS"; private static final String EXTRA_CHECKING_DURATION = "com.android.tv.action.extra.CHECKING_DURATION"; + private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP"; private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10); + private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification"; + // TODO: Load settings from XML file private static final TunerDevice[] TUNER_DEVICES = { new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q // WinTV-dualHD (bulk) will be supported after 2017 April security patch. new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk) - // STOPSHIP: Add WinTV-soloHD (Isoc) temporary for test. Remove this after test complete. new TunerDevice(0x2040, 0x0264, null), }; private static final int MSG_ENABLE_INPUT_SERVICE = 1000; private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; - /** - * Checks status of USB devices to see if there are available USB tuners connected. - */ - public static void onCheckingUsbTunerStatus(Context context, String action) { - onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler()); - } + private final ComponentName usbTunerComponent; + private final ComponentName networkTunerComponent; + private final ComponentName builtInTunerComponent; + private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>(); - private static void onCheckingUsbTunerStatus(Context context, String action, - @NonNull CheckDvbDeviceHandler handler) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - if (TunerHal.useBuiltInTuner(context)) { - enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN); - return; + private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>(); + private final Map<ComponentName, String> mNotificationMessages = new HashMap<>(); + private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>(); + + private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this); + + public TunerInputController(ComponentName embeddedTuner) { + usbTunerComponent = embeddedTuner; + networkTunerComponent = usbTunerComponent; + builtInTunerComponent = usbTunerComponent; + for (TunerDevice device : TUNER_DEVICES) { + mTunerServiceMapping.put(device, usbTunerComponent); } - // Falls back to the below to check USB tuner devices. - boolean enabled = isUsbTunerConnected(context); + } + + /** Checks status of USB devices to see if there are available USB tuners connected. */ + public void onCheckingUsbTunerStatus(Context context, String action) { + onCheckingUsbTunerStatus(context, action, mHandler); + } + + private void onCheckingUsbTunerStatus( + Context context, String action, @NonNull CheckDvbDeviceHandler handler) { + Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context); handler.removeMessages(MSG_ENABLE_INPUT_SERVICE); - if (enabled) { + if (!connectedUsbTuners.isEmpty()) { // Need to check if DVB driver is accessible. Since the driver creation // could be happen after the USB event, delay the checking by // DVB_DRIVER_CHECK_DELAY_MS. - handler.sendMessageDelayed(handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), + handler.sendMessageDelayed( + handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), DVB_DRIVER_CHECK_DELAY_MS); } else { - if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { - // Since network tuner is attached, do not disable TunerTvInput, - // just updates the TvInputInfo. - TunerInputInfoUtils.updateTunerInputInfo(context); - return; - } - enableTunerTvInputService(context, false, false, TextUtils - .equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) ? - TunerHal.TUNER_TYPE_USB : null); + handleTunerStatusChanged( + context, + false, + connectedUsbTuners, + TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) + ? TunerHal.TUNER_TYPE_USB + : null); } } - private static void onNetworkTunerChanged(Context context, boolean enabled) { + private void onNetworkTunerChanged(Context context, boolean enabled) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED) + && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) + == enabled) { + // the status is not changed + return; + } if (enabled) { - // Network tuner detection is initiated by UI. So the app should not - // be killed. sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply(); - enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK); } else { - sharedPreferences.edit() - .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply(); - if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) { - // Network tuner detection is initiated by UI. So the app should not - // be killed. - enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK); - } else { - // Since USB tuner is attached, do not disable TunerTvInput, - // just updates the TvInputInfo. - TunerInputInfoUtils.updateTunerInputInfo(context); - } + sharedPreferences + .edit() + .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) + .apply(); } + // Network tuner detection is initiated by UI. So the app should not + // be killed. + handleTunerStatusChanged( + context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK); } /** @@ -158,66 +179,131 @@ public class TunerInputController { * @param context {@link Context} instance * @return {@code true} if any tuner device we support is plugged in */ - private static boolean isUsbTunerConnected(Context context) { + private Set<TunerDevice> getConnectedUsbTuners(Context context) { UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); Map<String, UsbDevice> deviceList = manager.getDeviceList(); String currentSecurityLevel = SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null); + Set<TunerDevice> devices = new HashSet<>(); for (UsbDevice device : deviceList.values()) { if (DEBUG) { Log.d(TAG, "Device: " + device); } for (TunerDevice tuner : TUNER_DEVICES) { - if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) { + if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) { Log.i(TAG, "Tuner found"); - return true; + devices.add(tuner); } } } - return false; + return devices; + } + + private void handleTunerStatusChanged( + Context context, + boolean forceDontKillApp, + Set<TunerDevice> connectedUsbTuners, + Integer triggerType) { + Map<ComponentName, Integer> serviceToEnable = new HashMap<>(); + Set<ComponentName> serviceToDisable = new HashSet<>(); + serviceToDisable.add(builtInTunerComponent); + serviceToDisable.add(networkTunerComponent); + if (TunerFeatures.TUNER.isEnabled(context)) { + // TODO: support both built-in tuner and other tuners at the same time? + if (TunerHal.useBuiltInTuner(context)) { + enableTunerTvInputService( + context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent); + return; + } + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { + serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK); + } + } + for (TunerDevice device : TUNER_DEVICES) { + if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) { + serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB); + } else { + serviceToDisable.add(mTunerServiceMapping.get(device)); + } + } + serviceToDisable.removeAll(serviceToEnable.keySet()); + for (ComponentName serviceComponent : serviceToEnable.keySet()) { + if (isTunerPackageInstalled(context, serviceComponent)) { + enableTunerTvInputService( + context, + true, + forceDontKillApp, + serviceToEnable.get(serviceComponent), + serviceComponent); + } else { + sendNotificationToInstallPackage(context, serviceComponent); + } + } + for (ComponentName serviceComponent : serviceToDisable) { + if (isTunerPackageInstalled(context, serviceComponent)) { + enableTunerTvInputService( + context, false, forceDontKillApp, triggerType, serviceComponent); + } else { + cancelNotificationToInstallPackage(context, serviceComponent); + } + } } /** - * Enable/disable the component {@link TunerTvInputService}. + * Enable/disable the component {@link BaseTunerTvInputService}. * * @param context {@link Context} instance * @param enabled {@code true} to enable the service; otherwise {@code false} */ - private static void enableTunerTvInputService(Context context, boolean enabled, - boolean forceDontKillApp, Integer tunerType) { + private static void enableTunerTvInputService( + Context context, + boolean enabled, + boolean forceDontKillApp, + Integer tunerType, + ComponentName serviceComponent) { if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); - PackageManager pm = context.getPackageManager(); - ComponentName componentName = new ComponentName(context, TunerTvInputService.class); - - // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling - // TvActivity, the following pm.setComponentEnabledSetting doesn't work. - ((TvApplication) context.getApplicationContext()).handleInputCountChanged( - true, enabled, true); - // Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds - // (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only - // when the LiveChannels app is active since we don't want to kill the running app. - int flags = forceDontKillApp - || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() - ? PackageManager.DONT_KILL_APP : 0; - int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - if (newState != pm.getComponentEnabledSetting(componentName)) { - // Send/cancel the USB tuner TV input setup notification. - TunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); - // Enable/disable the USB tuner TV input. - pm.setComponentEnabledSetting(componentName, newState, flags); - if (!enabled && tunerType != null) { - if (tunerType == TunerHal.TUNER_TYPE_USB) { - Toast.makeText(context, R.string.msg_usb_tuner_disconnected, - Toast.LENGTH_SHORT).show(); - } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { - Toast.makeText(context, R.string.msg_network_tuner_disconnected, - Toast.LENGTH_SHORT).show(); + PackageManager pm = context.getPackageManager(); + int newState = + enabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + if (newState != pm.getComponentEnabledSetting(serviceComponent)) { + int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0; + if (serviceComponent.getPackageName().equals(context.getPackageName())) { + // Don't kill APP when handling input count changing. Or the following + // setComponentEnabledSetting() call won't work. + ((TvApplication) context.getApplicationContext()) + .handleInputCountChanged(true, enabled, true); + // Bundled input. Don't kill app if LiveChannels app is active since we don't want + // to kill the running app. + if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) { + flags |= PackageManager.DONT_KILL_APP; + } + // Send/cancel the USB tuner TV input setup notification. + BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); + if (!enabled && tunerType != null) { + if (tunerType == TunerHal.TUNER_TYPE_USB) { + Toast.makeText( + context, + R.string.msg_usb_tuner_disconnected, + Toast.LENGTH_SHORT) + .show(); + } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) { + Toast.makeText( + context, + R.string.msg_network_tuner_disconnected, + Toast.LENGTH_SHORT) + .show(); + } } } + // Enable/disable the USB tuner TV input. + pm.setComponentEnabledSetting(serviceComponent, newState, flags); if (DEBUG) Log.d(TAG, "Status updated:" + enabled); - } else if (enabled) { + } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) { // When # of tuners is changed or the tuner input service is switching from/to using // network tuners or the device just boots. TunerInputInfoUtils.updateTunerInputInfo(context); @@ -227,96 +313,179 @@ public class TunerInputController { /** * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. */ - public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) { - boolean runningInMainProcess = - TvApplication.getSingletons(context).isRunningInMainProcess(); - SoftPreconditions.checkState(runningInMainProcess); - if (!runningInMainProcess) { - return; - } - executeNetworkTunerDiscoveryAsyncTask(context, 0); + public void executeNetworkTunerDiscoveryAsyncTask(final Context context) { + executeNetworkTunerDiscoveryAsyncTask(context, 0, 0); } /** * Discovers a network tuner. + * * @param context {@link Context} - * @param repeatedDurationMs the time length to wait to repeatedly check network status to start - * finding network tuner when the network connection is not available. - * {@code 0} to disable repeatedly checking. + * @param repeatedDurationMs The time length to wait to repeatedly check network status to start + * finding network tuner when the network connection is not available. {@code 0} to disable + * repeatedly checking. + * @param deviceIp The previous discovered device IP, 0 if none. */ - private static void executeNetworkTunerDiscoveryAsyncTask(final Context context, - final long repeatedDurationMs) { - if (!Features.NETWORK_TUNER.isEnabled(context)) { + private void executeNetworkTunerDiscoveryAsyncTask( + final Context context, final long repeatedDurationMs, final int deviceIp) { + if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) { return; } - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... params) { - if (isNetworkConnected(context)) { + final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); + networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS); + if (!isNetworkConnected(context) && repeatedDurationMs > 0) { + sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs); + } else { + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + Boolean result = null; // Implement and execute network tuner discovery AsyncTask here. - } else if (repeatedDurationMs > 0) { - AlarmManager alarmManager = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); - networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION); - networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs); - PendingIntent alarmIntent = PendingIntent.getBroadcast( - context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() - + repeatedDurationMs, alarmIntent); + return result; } - return null; - } - @Override - protected void onPostExecute(Boolean result) { - if (result == null) { - return; + @Override + protected void onPostExecute(Boolean foundNetworkTuner) { + if (foundNetworkTuner == null) { + return; + } + sendCheckingAlarm( + context, + networkCheckingIntent, + foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs); + onNetworkTunerChanged(context, foundNetworkTuner); } - onNetworkTunerChanged(context, result); - } - }.execute(); + }.execute(); + } } private static boolean isNetworkConnected(Context context) { - ConnectivityManager cm = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = cm.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } + private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + intent.putExtra(EXTRA_CHECKING_DURATION, delayMs); + PendingIntent alarmIntent = + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + delayMs, + alarmIntent); + } + + private static boolean isTunerPackageInstalled( + Context context, ComponentName serviceComponent) { + try { + context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0); + return true; + } catch (NameNotFoundException e) { + return false; + } + } + + private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) { + if (!BuildConfig.ENG) { + return; + } + String applicationName = mTunerApplicationNames.get(serviceComponent); + if (applicationName == null) { + applicationName = context.getString(R.string.tuner_install_default_application_name); + } + String contentTitle = + context.getString( + R.string.tuner_install_notification_content_title, applicationName); + String contentText = mNotificationMessages.get(serviceComponent); + if (contentText == null) { + contentText = context.getString(R.string.tuner_install_notification_content_text); + } + Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent); + if (largeIcon == null) { + // TODO: Make a better default image. + largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store); + } + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) { + createNotificationChannel(context, notificationManager); + } + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData( + Uri.parse( + String.format( + PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName()))); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); + builder.setAutoCancel(true) + .setSmallIcon(R.drawable.ic_launcher_s) + .setLargeIcon(largeIcon) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setCategory(Notification.CATEGORY_RECOMMENDATION) + .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); + notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build()); + } + + private static void cancelNotificationToInstallPackage( + Context context, ComponentName serviceComponent) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(serviceComponent.getPackageName(), 0); + } + + private static void createNotificationChannel( + Context context, NotificationManager notificationManager) { + notificationManager.createNotificationChannel( + new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + context.getResources() + .getString(R.string.ut_setup_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH)); + } + public static class IntentReceiver extends BroadcastReceiver { - private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(); @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); - TvApplication.setCurrentRunningProcess(context, true); - if (!Features.TUNER.isEnabled(context)) { - enableTunerTvInputService(context, false, false, null); + Starter.start(context); + TunerInputController tunerInputController = + TvSingletons.getSingletons(context).getTunerInputController(); + if (!TunerFeatures.TUNER.isEnabled(context)) { + tunerInputController.handleTunerStatusChanged( + context, false, Collections.emptySet(), null); return; } switch (intent.getAction()) { case Intent.ACTION_BOOT_COMPLETED: - executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS); + tunerInputController.executeNetworkTunerDiscoveryAsyncTask( + context, INITIAL_CHECKING_DURATION_MS, 0); + // fall through case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: case UsbManager.ACTION_USB_DEVICE_ATTACHED: case UsbManager.ACTION_USB_DEVICE_DETACHED: - onCheckingUsbTunerStatus(context, intent.getAction(), mHandler); + tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction()); break; - case CHECKING_NETWORK_CONNECTION: - long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION, - INITIAL_CHECKING_DURATION_MS); - executeNetworkTunerDiscoveryAsyncTask(context, - Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS)); + case CHECKING_NETWORK_TUNER_STATUS: + long repeatedDurationMs = + intent.getLongExtra( + EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS); + tunerInputController.executeNetworkTunerDiscoveryAsyncTask( + context, + Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS), + intent.getIntExtra(EXTRA_DEVICE_IP, 0)); break; + default: // fall out } } } /** - * Simple data holder for a USB device. Used to represent a tuner model, and compare - * against {@link UsbDevice}. + * Simple data holder for a USB device. Used to represent a tuner model, and compare against + * {@link UsbDevice}. */ private static class TunerDevice { private final int vendorId; @@ -331,7 +500,7 @@ public class TunerInputController { this.minSecurityLevel = minSecurityLevel; } - private boolean equals(UsbDevice device) { + private boolean equalsTo(UsbDevice device) { return device.getVendorId() == vendorId && device.getProductId() == productId; } @@ -354,10 +523,13 @@ public class TunerInputController { } private static class CheckDvbDeviceHandler extends Handler { + + private final TunerInputController mTunerInputController; private DvbDeviceAccessor mDvbDeviceAccessor; - CheckDvbDeviceHandler() { + CheckDvbDeviceHandler(TunerInputController tunerInputController) { super(Looper.getMainLooper()); + this.mTunerInputController = tunerInputController; } @Override @@ -369,9 +541,15 @@ public class TunerInputController { mDvbDeviceAccessor = new DvbDeviceAccessor(context); } boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); - enableTunerTvInputService( - context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null); + mTunerInputController.handleTunerStatusChanged( + context, + false, + enabled + ? mTunerInputController.getConnectedUsbTuners(context) + : Collections.emptySet(), + TunerHal.TUNER_TYPE_USB); break; + default: // fall out } } } diff --git a/src/com/android/tv/tuner/TunerPreferenceProvider.java b/src/com/android/tv/tuner/TunerPreferenceProvider.java deleted file mode 100644 index 3a3561b6..00000000 --- a/src/com/android/tv/tuner/TunerPreferenceProvider.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; - -/** - * A content provider for storing the preferences. It's used across TV app and USB tuner TV input. - */ -public class TunerPreferenceProvider extends ContentProvider { - /** The authority of the provider */ - public static final String AUTHORITY = "com.android.tv.tuner.preferences"; - - private static final String PATH_PREFERENCES = "preferences"; - - private static final int DATABASE_VERSION = 1; - private static final String DATABASE_NAME = "usbtuner_preferences.db"; - private static final String PREFERENCES_TABLE = "preferences"; - - private static final int MATCH_PREFERENCE = 1; - private static final int MATCH_PREFERENCE_KEY = 2; - - private static final UriMatcher sUriMatcher; - - private DatabaseOpenHelper mDatabaseOpenHelper; - - static { - sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - sUriMatcher.addURI(AUTHORITY, "preferences", MATCH_PREFERENCE); - sUriMatcher.addURI(AUTHORITY, "preferences/*", MATCH_PREFERENCE_KEY); - } - - /** - * Builds a Uri that points to a specific preference. - - * @param key a key of the preference to point to - */ - public static Uri buildPreferenceUri(String key) { - return Preferences.CONTENT_URI.buildUpon().appendPath(key).build(); - } - - /** - * Columns definitions for the preferences table. - */ - public interface Preferences { - - /** - * The content:// style for the preferences table. - */ - Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREFERENCES); - - /** - * The MIME type of a directory of preferences. - */ - String CONTENT_TYPE = "vnd.android.cursor.dir/preferences"; - - /** - * The MIME type of a single preference. - */ - String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/preferences"; - - /** - * The ID of this preference. - * - * <p>This is auto-incremented. - * - * <p>Type: INTEGER - */ - String _ID = "_id"; - - /** - * The key of this preference. - * - * <p>Should be unique. - * - * <p>Type: TEXT - */ - String COLUMN_KEY = "key"; - - /** - * The value of this preference. - * - * <p>Type: TEXT - */ - String COLUMN_VALUE = "value"; - } - - private static class DatabaseOpenHelper extends SQLiteOpenHelper { - public DatabaseOpenHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + PREFERENCES_TABLE + " (" - + Preferences._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + Preferences.COLUMN_KEY + " TEXT NOT NULL," - + Preferences.COLUMN_VALUE + " TEXT);"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // No-op - } - } - - @Override - public boolean onCreate() { - mDatabaseOpenHelper = new DatabaseOpenHelper(getContext()); - return true; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - int match = sUriMatcher.match(uri); - if (match != MATCH_PREFERENCE && match != MATCH_PREFERENCE_KEY) { - throw new UnsupportedOperationException(); - } - SQLiteDatabase db = mDatabaseOpenHelper.getReadableDatabase(); - Cursor cursor = db.query(PREFERENCES_TABLE, projection, selection, selectionArgs, - null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @Override - public String getType(Uri uri) { - switch (sUriMatcher.match(uri)) { - case MATCH_PREFERENCE: - return Preferences.CONTENT_TYPE; - case MATCH_PREFERENCE_KEY: - return Preferences.CONTENT_ITEM_TYPE; - } - throw new IllegalArgumentException("Unknown URI " + uri); - } - - /** - * Inserts a preference row into the preference table. - * - * If a key is already exists in the table, it removes the old row and inserts a new row. - * - * @param uri the URL of the table to insert into - * @param values the initial values for the newly inserted row - * @return the URL of the newly created row - */ - @Override - public Uri insert(Uri uri, ContentValues values) { - if (sUriMatcher.match(uri) != MATCH_PREFERENCE) { - throw new UnsupportedOperationException(); - } - return insertRow(uri, values); - } - - private Uri insertRow(Uri uri, ContentValues values) { - SQLiteDatabase db = mDatabaseOpenHelper.getWritableDatabase(); - - // Remove the old row. - db.delete(PREFERENCES_TABLE, Preferences.COLUMN_KEY + " like ?", - new String[]{values.getAsString(Preferences.COLUMN_KEY)}); - - long rowId = db.insert(PREFERENCES_TABLE, null, values); - if (rowId > 0) { - Uri rowUri = buildPreferenceUri(values.getAsString(Preferences.COLUMN_KEY)); - getContext().getContentResolver().notifyChange(rowUri, null); - return rowUri; - } - - throw new SQLiteException("Failed to insert row into " + uri); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } -} diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java deleted file mode 100644 index 11a6a969..00000000 --- a/src/com/android/tv/tuner/TunerPreferences.java +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.GuardedBy; -import android.support.annotation.IntDef; -import android.support.annotation.MainThread; - -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.TunerPreferenceProvider.Preferences; -import com.android.tv.tuner.util.TisConfiguration; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * A helper class for the USB tuner preferences. - */ -public class TunerPreferences { - private static final String TAG = "TunerPreferences"; - - private static final String PREFS_KEY_CHANNEL_DATA_VERSION = "channel_data_version"; - private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count"; - private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code"; - private static final String PREFS_KEY_SCAN_DONE = "scan_done"; - private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup"; - private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream"; - private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting"; - private static final String PREFS_KEY_TRICKPLAY_EXPIRED_MS = "trickplay_expired_ms"; - - private static final String SHARED_PREFS_NAME = "com.android.tv.tuner.preferences"; - - public static final int CHANNEL_DATA_VERSION_NOT_SET = -1; - - @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED}) - @Retention(RetentionPolicy.SOURCE) - public @interface TrickplaySetting { - } - - /** - * Trickplay setting is not changed by a user. Trickplay will be enabled in this case. - */ - public static final int TRICKPLAY_SETTING_NOT_SET = -1; - - /** - * Trickplay setting is disabled. - */ - public static final int TRICKPLAY_SETTING_DISABLED = 0; - - /** - * Trickplay setting is enabled. - */ - public static final int TRICKPLAY_SETTING_ENABLED = 1; - - @GuardedBy("TunerPreferences.class") - private static final Bundle sPreferenceValues = new Bundle(); - private static LoadPreferencesTask sLoadPreferencesTask; - private static ContentObserver sContentObserver; - private static TunerPreferencesChangedListener sPreferencesChangedListener = null; - - private static boolean sInitialized; - - /** - * Listeners for TunerPreferences change. - */ - public interface TunerPreferencesChangedListener { - void onTunerPreferencesChanged(); - } - - /** - * Initializes the USB tuner preferences. - */ - @MainThread - public static void initialize(final Context context) { - if (sInitialized) { - return; - } - sInitialized = true; - if (useContentProvider(context)) { - loadPreferences(context); - sContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - loadPreferences(context); - } - }; - context.getContentResolver().registerContentObserver( - TunerPreferenceProvider.Preferences.CONTENT_URI, true, sContentObserver); - } else { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - getSharedPreferences(context); - return null; - } - }.execute(); - } - } - - /** - * Releases the resources. - */ - public static synchronized void release(Context context) { - if (useContentProvider(context) && sContentObserver != null) { - context.getContentResolver().unregisterContentObserver(sContentObserver); - } - setTunerPreferencesChangedListener(null); - } - - /** - * Sets the listener for TunerPreferences change. - */ - public static void setTunerPreferencesChangedListener( - TunerPreferencesChangedListener listener) { - sPreferencesChangedListener = listener; - } - - /** - * Loads the preferences from database. - * <p> - * This preferences is used across processes, so the preferences should be loaded again when the - * databases changes. - */ - @MainThread - public static void loadPreferences(Context context) { - if (sLoadPreferencesTask != null - && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) { - sLoadPreferencesTask.cancel(true); - } - sLoadPreferencesTask = new LoadPreferencesTask(context); - sLoadPreferencesTask.execute(); - } - - private static boolean useContentProvider(Context context) { - // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access. - return TisConfiguration.isPackagedWithLiveChannels(context); - } - - public static synchronized int getChannelDataVersion(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION, - CHANNEL_DATA_VERSION_NOT_SET); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION, - CHANNEL_DATA_VERSION_NOT_SET); - } - } - - public static synchronized void setChannelDataVersion(Context context, int version) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION, version) - .apply(); - } - } - - public static synchronized int getScannedChannelCount(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, 0); - } - } - - public static synchronized void setScannedChannelCount(Context context, int channelCount) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount) - .apply(); - } - } - - public static synchronized String getLastPostalCode(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE); - } else { - return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null); - } - } - - public static synchronized void setLastPostalCode(Context context, String postalCode) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode); - } else { - getSharedPreferences(context).edit() - .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode).apply(); - } - } - - public static synchronized boolean isScanDone(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getBoolean(PREFS_KEY_SCAN_DONE); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, false); - } - } - - public static synchronized void setScanDone(Context context) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_SCAN_DONE, true); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, true) - .apply(); - } - } - - public static synchronized boolean shouldShowSetupActivity(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, false); - } - } - - public static synchronized void setShouldShowSetupActivity(Context context, boolean need) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_LAUNCH_SETUP, need); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, need) - .apply(); - } - } - - public static synchronized long getTrickplayExpiredMs(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getLong(PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0); - } else { - return getSharedPreferences(context) - .getLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0); - } - } - - public static synchronized void setTrickplayExpiredMs(Context context, long timeMs) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs); - } else { - getSharedPreferences(context).edit() - .putLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs) - .apply(); - } - } - - public static synchronized @TrickplaySetting int getTrickplaySetting(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); - } - } - - public static synchronized void setTrickplaySetting(Context context, - @TrickplaySetting int trickplaySetting) { - SoftPreconditions.checkState(sInitialized); - SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET); - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting) - .apply(); - } - } - - public static synchronized boolean getStoreTsStream(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, false); - } - } - - public static synchronized void setStoreTsStream(Context context, boolean shouldStore) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, shouldStore) - .apply(); - } - } - - private static SharedPreferences getSharedPreferences(Context context) { - return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - } - - private static synchronized void setPreference(Context context, String key, String value) { - sPreferenceValues.putString(key, value); - savePreference(context, key, value); - } - - private static synchronized void setPreference(Context context, String key, int value) { - sPreferenceValues.putInt(key, value); - savePreference(context, key, Integer.toString(value)); - } - - private static synchronized void setPreference(Context context, String key, long value) { - sPreferenceValues.putLong(key, value); - savePreference(context, key, Long.toString(value)); - } - - private static synchronized void setPreference(Context context, String key, boolean value) { - sPreferenceValues.putBoolean(key, value); - savePreference(context, key, Boolean.toString(value)); - } - - private static void savePreference(final Context context, final String key, - final String value) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - ContentResolver resolver = context.getContentResolver(); - ContentValues values = new ContentValues(); - values.put(Preferences.COLUMN_KEY, key); - values.put(Preferences.COLUMN_VALUE, value); - try { - resolver.insert(Preferences.CONTENT_URI, values); - } catch (Exception e) { - SoftPreconditions.warn(TAG, "setPreference", "Error writing preference values", - e); - } - return null; - } - }.execute(); - } - - private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> { - private final Context mContext; - private LoadPreferencesTask(Context context) { - mContext = context; - } - - @Override - protected Bundle doInBackground(Void... params) { - Bundle bundle = new Bundle(); - ContentResolver resolver = mContext.getContentResolver(); - String[] projection = new String[] { Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE }; - try (Cursor cursor = resolver.query(Preferences.CONTENT_URI, projection, null, null, - null)) { - if (cursor != null) { - while (!isCancelled() && cursor.moveToNext()) { - String key = cursor.getString(0); - String value = cursor.getString(1); - switch (key) { - case PREFS_KEY_TRICKPLAY_EXPIRED_MS: - bundle.putLong(key, Long.parseLong(value)); - break; - case PREFS_KEY_CHANNEL_DATA_VERSION: - case PREFS_KEY_SCANNED_CHANNEL_COUNT: - case PREFS_KEY_TRICKPLAY_SETTING: - try { - bundle.putInt(key, Integer.parseInt(value)); - } catch (NumberFormatException e) { - // Does nothing. - } - break; - case PREFS_KEY_SCAN_DONE: - case PREFS_KEY_LAUNCH_SETUP: - case PREFS_KEY_STORE_TS_STREAM: - bundle.putBoolean(key, Boolean.parseBoolean(value)); - break; - case PREFS_KEY_LAST_POSTAL_CODE: - bundle.putString(key, value); - break; - } - } - } - } catch (Exception e) { - SoftPreconditions.warn(TAG, "getPreference", "Error querying preference values", e); - return null; - } - return bundle; - } - - @Override - protected void onPostExecute(Bundle bundle) { - synchronized (TunerPreferences.class) { - if (bundle != null) { - sPreferenceValues.putAll(bundle); - } - } - if (sPreferencesChangedListener != null) { - sPreferencesChangedListener.onTunerPreferencesChanged(); - } - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java deleted file mode 100644 index a88538df..00000000 --- a/src/com/android/tv/tuner/cc/CaptionLayout.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.cc; - -import android.content.Context; -import android.util.AttributeSet; - -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.layout.ScaledLayout; - -/** - * Layout containing the safe title area that helps the closed captions look more prominent. - * This is required by CEA-708B. - */ -public class CaptionLayout extends ScaledLayout { - // The safe title area has 10% margins of the screen. - private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f; - private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f; - private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f; - private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f; - - private final ScaledLayout mSafeTitleAreaLayout; - private AtscCaptionTrack mCaptionTrack; - - public CaptionLayout(Context context) { - this(context, null); - } - - public CaptionLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public CaptionLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - mSafeTitleAreaLayout = new ScaledLayout(context); - addView(mSafeTitleAreaLayout, new ScaledLayoutParams( - SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X, - SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y)); - } - - public void addOrUpdateViewToSafeTitleArea(CaptionWindowLayout captionWindowLayout, - ScaledLayoutParams scaledLayoutParams) { - int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout); - if (index < 0) { - mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams); - return; - } - mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams); - } - - public void removeViewFromSafeTitleArea(CaptionWindowLayout captionWindowLayout) { - mSafeTitleAreaLayout.removeView(captionWindowLayout); - } - - public void setCaptionTrack(AtscCaptionTrack captionTrack) { - mCaptionTrack = captionTrack; - } - - public AtscCaptionTrack getCaptionTrack() { - return mCaptionTrack; - } -} diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java deleted file mode 100644 index 24a0f354..00000000 --- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.cc; - -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.view.View; - -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; -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.nano.Track.AtscCaptionTrack; - -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -/** - * Decodes and renders CEA-708. - */ -public class CaptionTrackRenderer implements Handler.Callback { - // TODO: Remaining works - // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are - // described in the follows. - // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized but - // it is handled as EUC-KR charset for korea broadcasting. - // C1 Table: All styles of windows and pens except underline, italic, pen size, and pen offset - // specified in CEA-708 are ignored and this follows system wide cc preferences for - // look and feel. SetPenLocation is not implemented. - // G2 Table: TSP, NBTSP and BLK are not supported. - // Text/commands: Word wrapping, fonts, row and column locking are not supported. - - private static final String TAG = "CaptionTrackRenderer"; - private static final boolean DEBUG = false; - - private static final long DELAY_IN_MILLIS = TimeUnit.MILLISECONDS.toMillis(100); - - // According to CEA-708B, there can exist up to 8 caption windows. - private static final int CAPTION_WINDOWS_MAX = 8; - private static final int CAPTION_ALL_WINDOWS_BITMAP = 255; - - private static final int MSG_DELAY_CANCEL = 1; - private static final int MSG_CAPTION_CLEAR = 2; - - private static final long CAPTION_CLEAR_INTERVAL_MS = 60000; - - private final CaptionLayout mCaptionLayout; - private boolean mIsDelayed = false; - private CaptionWindowLayout mCurrentWindowLayout; - private final CaptionWindowLayout[] mCaptionWindowLayouts = - new CaptionWindowLayout[CAPTION_WINDOWS_MAX]; - private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>(); - private final Handler mHandler; - - public CaptionTrackRenderer(CaptionLayout captionLayout) { - mCaptionLayout = captionLayout; - mHandler = new Handler(this); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_DELAY_CANCEL: - delayCancel(); - return true; - case MSG_CAPTION_CLEAR: - clearWindows(CAPTION_ALL_WINDOWS_BITMAP); - return true; - } - return false; - } - - public void start(AtscCaptionTrack captionTrack) { - if (captionTrack == null) { - stop(); - return; - } - if (DEBUG) { - Log.d(TAG, "Start captionTrack " + captionTrack.language); - } - reset(); - mCaptionLayout.setCaptionTrack(captionTrack); - mCaptionLayout.setVisibility(View.VISIBLE); - } - - public void stop() { - if (DEBUG) { - Log.d(TAG, "Stop captionTrack"); - } - mCaptionLayout.setVisibility(View.INVISIBLE); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - } - - public void processCaptionEvent(CaptionEvent event) { - if (mIsDelayed) { - mPendingCaptionEvents.add(event); - return; - } - switch (event.type) { - case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER: - sendBufferToCurrentWindow((String) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL: - sendControlToCurrentWindow((char) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX: - setCurrentWindowLayout((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW: - clearWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW: - displayWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW: - hideWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW: - toggleWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW: - deleteWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY: - delay((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC: - delayCancel(); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST: - reset(); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA: - setPenAttr((CaptionPenAttr) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC: - setPenColor((CaptionPenColor) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL: - setPenLocation((CaptionPenLocation) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA: - setWindowAttr((CaptionWindowAttr) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX: - defineWindow((CaptionWindow) event.obj); - break; - } - } - - // The window related caption commands - private void setCurrentWindowLayout(int windowId) { - if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { - return; - } - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; - if (windowLayout == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "setCurrentWindowLayout to " + windowId); - } - mCurrentWindowLayout = windowLayout; - } - - // Each bit of windowBitmap indicates a window. - // If a bit is set, the window id is the same as the number of the trailing zeros of the bit. - private ArrayList<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) { - ArrayList<CaptionWindowLayout> windows = new ArrayList<>(); - for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { - if ((windowBitmap & (1 << i)) != 0) { - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i]; - if (windowLayout != null) { - windows.add(windowLayout); - } - } - } - return windows; - } - - private void clearWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.clear(); - } - } - - private void displayWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.show(); - } - } - - private void hideWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.hide(); - } - } - - private void toggleWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - if (windowLayout.isShown()) { - windowLayout.hide(); - } else { - windowLayout.show(); - } - } - } - - private void deleteWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.removeFromCaptionView(); - mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null; - } - } - - public void clear() { - mHandler.sendEmptyMessage(MSG_CAPTION_CLEAR); - } - - public void reset() { - mCurrentWindowLayout = null; - mIsDelayed = false; - mPendingCaptionEvents.clear(); - for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { - if (mCaptionWindowLayouts[i] != null) { - mCaptionWindowLayouts[i].removeFromCaptionView(); - } - mCaptionWindowLayouts[i] = null; - } - mCaptionLayout.setVisibility(View.INVISIBLE); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - } - - private void setWindowAttr(CaptionWindowAttr windowAttr) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setWindowAttr(windowAttr); - } - } - - private void defineWindow(CaptionWindow window) { - if (window == null) { - return; - } - int windowId = window.id; - if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { - return; - } - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; - if (windowLayout == null) { - windowLayout = new CaptionWindowLayout(mCaptionLayout.getContext()); - } - windowLayout.initWindow(mCaptionLayout, window); - mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout; - } - - // The job related caption commands - private void delay(int tenthsOfSeconds) { - if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) { - return; - } - mIsDelayed = true; - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL), - tenthsOfSeconds * DELAY_IN_MILLIS); - } - - private void delayCancel() { - mIsDelayed = false; - processPendingBuffer(); - } - - private void processPendingBuffer() { - for (CaptionEvent event : mPendingCaptionEvents) { - processCaptionEvent(event); - } - mPendingCaptionEvents.clear(); - } - - // The implicit write caption commands - private void sendControlToCurrentWindow(char control) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.sendControl(control); - } - } - - private void sendBufferToCurrentWindow(String buffer) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.sendBuffer(buffer); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR), - CAPTION_CLEAR_INTERVAL_MS); - } - } - - // The pen related caption commands - private void setPenAttr(CaptionPenAttr attr) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenAttr(attr); - } - } - - private void setPenColor(CaptionPenColor color) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenColor(color); - } - } - - private void setPenLocation(CaptionPenLocation location) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenLocation(location.row, location.column); - } - } -} diff --git a/src/com/android/tv/tuner/cc/CaptionWindowLayout.java b/src/com/android/tv/tuner/cc/CaptionWindowLayout.java deleted file mode 100644 index 6f42b506..00000000 --- a/src/com/android/tv/tuner/cc/CaptionWindowLayout.java +++ /dev/null @@ -1,650 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.cc; - -import android.content.Context; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.CharacterStyle; -import android.text.style.RelativeSizeSpan; -import android.text.style.StyleSpan; -import android.text.style.SubscriptSpan; -import android.text.style.SuperscriptSpan; -import android.text.style.UnderlineSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.CaptioningManager; -import android.view.accessibility.CaptioningManager.CaptionStyle; -import android.view.accessibility.CaptioningManager.CaptioningChangeListener; -import android.widget.RelativeLayout; - -import com.google.android.exoplayer.text.CaptionStyleCompat; -import com.google.android.exoplayer.text.SubtitleView; -import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; -import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; -import com.android.tv.tuner.data.Cea708Data.CaptionWindow; -import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; -import com.android.tv.tuner.layout.ScaledLayout; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that - * takes care of displaying the actual cc text. - */ -public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener { - private static final String TAG = "CaptionWindowLayout"; - private static final boolean DEBUG = false; - - private static final float PROPORTION_PEN_SIZE_SMALL = .75f; - private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f; - - // The following values indicates the maximum cell number of a window. - private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99; - private static final int ANCHOR_VERTICAL_MAX = 74; - private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159; - private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209; - - // The following values indicates a gravity of a window. - private static final int ANCHOR_MODE_DIVIDER = 3; - private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0; - private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1; - private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2; - private static final int ANCHOR_VERTICAL_MODE_TOP = 0; - private static final int ANCHOR_VERTICAL_MODE_CENTER = 1; - private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2; - - private static final int US_MAX_COLUMN_COUNT_16_9 = 42; - private static final int US_MAX_COLUMN_COUNT_4_3 = 32; - private static final int KR_MAX_COLUMN_COUNT_16_9 = 52; - private static final int KR_MAX_COLUMN_COUNT_4_3 = 40; - private static final int MAX_ROW_COUNT = 15; - - private static final String KOR_ALPHABET = - new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f; - - private CaptionLayout mCaptionLayout; - private CaptionStyleCompat mCaptionStyleCompat; - - // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}. - private final SubtitleView mSubtitleView; - private int mRowLimit = 0; - private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); - private final List<CharacterStyle> mCharacterStyles = new ArrayList<>(); - private int mCaptionWindowId; - private int mCurrentTextRow = -1; - private float mFontScale; - private float mTextSize; - private String mWidestChar; - private int mLastCaptionLayoutWidth; - private int mLastCaptionLayoutHeight; - private int mWindowJustify; - private int mPrintDirection; - - private class SystemWideCaptioningChangeListener extends CaptioningChangeListener { - @Override - public void onUserStyleChanged(CaptionStyle userStyle) { - mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle); - mSubtitleView.setStyle(mCaptionStyleCompat); - updateWidestChar(); - } - - @Override - public void onFontScaleChanged(float fontScale) { - mFontScale = fontScale; - updateTextSize(); - } - } - - public CaptionWindowLayout(Context context) { - this(context, null); - } - - public CaptionWindowLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - // Add a subtitle view to the layout. - mSubtitleView = new SubtitleView(context); - LayoutParams params = new RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - addView(mSubtitleView, params); - - // Set the system wide cc preferences to the subtitle view. - CaptioningManager captioningManager = - (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); - mFontScale = captioningManager.getFontScale(); - mCaptionStyleCompat = - CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); - mSubtitleView.setStyle(mCaptionStyleCompat); - mSubtitleView.setText(""); - captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener()); - updateWidestChar(); - } - - public int getCaptionWindowId() { - return mCaptionWindowId; - } - - public void setCaptionWindowId(int captionWindowId) { - mCaptionWindowId = captionWindowId; - } - - public void clear() { - clearText(); - hide(); - } - - public void show() { - setVisibility(View.VISIBLE); - requestLayout(); - } - - public void hide() { - setVisibility(View.INVISIBLE); - requestLayout(); - } - - public void setPenAttr(CaptionPenAttr penAttr) { - mCharacterStyles.clear(); - if (penAttr.italic) { - mCharacterStyles.add(new StyleSpan(Typeface.ITALIC)); - } - if (penAttr.underline) { - mCharacterStyles.add(new UnderlineSpan()); - } - switch (penAttr.penSize) { - case CaptionPenAttr.PEN_SIZE_SMALL: - mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL)); - break; - case CaptionPenAttr.PEN_SIZE_LARGE: - mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE)); - break; - } - switch (penAttr.penOffset) { - case CaptionPenAttr.OFFSET_SUBSCRIPT: - mCharacterStyles.add(new SubscriptSpan()); - break; - case CaptionPenAttr.OFFSET_SUPERSCRIPT: - mCharacterStyles.add(new SuperscriptSpan()); - break; - } - } - - public void setPenColor(CaptionPenColor penColor) { - // TODO: apply pen colors or skip this and use the style of system wide cc style as is. - } - - public void setPenLocation(int row, int column) { - // TODO: change the location of pen when window's justify isn't left. - // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within - // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate - // at row. Adding white space to make cursor locate at column. - if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) { - if (mCurrentTextRow >= 0) { - for (int r = mCurrentTextRow; r < row; ++r) { - appendText("\n"); - } - if (mCurrentTextRow <= row) { - for (int i = 0; i < column; ++i) { - appendText(" "); - } - } - } - } - mCurrentTextRow = row; - } - - public void setWindowAttr(CaptionWindowAttr windowAttr) { - // TODO: apply window attrs or skip this and use the style of system wide cc style as is. - mWindowJustify = windowAttr.justify; - mPrintDirection = windowAttr.printDirection; - } - - public void sendBuffer(String buffer) { - appendText(buffer); - } - - public void sendControl(char control) { - // TODO: there are a bunch of ASCII-style control codes. - } - - /** - * This method places the window on a given CaptionLayout along with the anchor of the window. - * <p> - * According to CEA-708B, the anchor id indicates the gravity of the window as the follows. - * For example, A value 7 of a anchor id says that a window is align with its parent bottom and - * is located at the center horizontally of its parent. - * </p> - * <h4>Anchor id and the gravity of a window</h4> - * <table> - * <tr> - * <th>GRAVITY</th> - * <th>LEFT</th> - * <th>CENTER_HORIZONTAL</th> - * <th>RIGHT</th> - * </tr> - * <tr> - * <th>TOP</th> - * <td>0</td> - * <td>1</td> - * <td>2</td> - * </tr> - * <tr> - * <th>CENTER_VERTICAL</th> - * <td>3</td> - * <td>4</td> - * <td>5</td> - * </tr> - * <tr> - * <th>BOTTOM</th> - * <td>6</td> - * <td>7</td> - * <td>8</td> - * </tr> - * </table> - * <p> - * In order to handle the gravity of a window, there are two steps. First, set the size of the - * window. Since the window will be positioned at {@link ScaledLayout}, the size factors are - * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is - * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view, - * {@link SubtitleView}. - * </p> - * <p> - * The gravity of the window is also related to its size. When it should be pushed to a one of - * the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a boundary - * of the window. When it should be pushed in the horizontal/vertical center of its container, - * the horizontal/vertical center point of the window should be the same as the anchor point. - * </p> - * - * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area - * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the - * window - */ - public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) { - if (DEBUG) { - Log.d(TAG, "initWindow with " - + (captionLayout != null ? captionLayout.getCaptionTrack() : null)); - } - if (mCaptionLayout != captionLayout) { - if (mCaptionLayout != null) { - mCaptionLayout.removeOnLayoutChangeListener(this); - } - mCaptionLayout = captionLayout; - mCaptionLayout.addOnLayoutChangeListener(this); - updateWidestChar(); - } - - // Both anchor vertical and horizontal indicates the position cell number of the window. - float scaleRow = (float) captionWindow.anchorVertical / (captionWindow.relativePositioning - ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX); - float scaleCol = (float) captionWindow.anchorHorizontal / - (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX - : (isWideAspectRatio() - ? ANCHOR_HORIZONTAL_16_9_MAX : ANCHOR_HORIZONTAL_4_3_MAX)); - - // The range of scaleRow/Col need to be verified to be in [0, 1]. - // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}. - if (scaleRow < 0 || scaleRow > 1) { - Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 and 1" - + " but " + scaleRow); - scaleRow = Math.max(0, Math.min(scaleRow, 1)); - } - if (scaleCol < 0 || scaleCol > 1) { - Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0 and" - + " 1 but " + scaleCol); - scaleCol = Math.max(0, Math.min(scaleCol, 1)); - } - int gravity = Gravity.CENTER; - int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER; - int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER; - float scaleStartRow = 0; - float scaleEndRow = 1; - float scaleStartCol = 0; - float scaleEndCol = 1; - switch (horizontalMode) { - case ANCHOR_HORIZONTAL_MODE_LEFT: - gravity = Gravity.LEFT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL); - scaleStartCol = scaleCol; - break; - case ANCHOR_HORIZONTAL_MODE_CENTER: - float gap = Math.min(1 - scaleCol, scaleCol); - - // Since all TV sets use left text alignment instead of center text alignment - // for this case, we follow the industry convention if possible. - int columnCount = captionWindow.columnCount + 1; - if (isKoreanLanguageTrack()) { - columnCount /= 2; - } - columnCount = Math.min(getScreenColumnCount(), columnCount); - StringBuilder widestTextBuilder = new StringBuilder(); - for (int i = 0; i < columnCount; ++i) { - widestTextBuilder.append(mWidestChar); - } - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - paint.setTextSize(mTextSize); - float maxWindowWidth = paint.measureText(widestTextBuilder.toString()); - float halfMaxWidthScale = mCaptionLayout.getWidth() > 0 - ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f) : 0.0f; - if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) { - // Calculate the expected max window size based on the column count of the - // caption window multiplied by average alphabets char width, then align the - // left side of the window with the left side of the expected max window. - gravity = Gravity.LEFT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL); - scaleStartCol = scaleCol - halfMaxWidthScale; - scaleEndCol = 1.0f; - } else { - // The gap will be the minimum distance value of the distances from both - // horizontal end points to the anchor point. - // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2]. - // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1]. - // The anchor point is located at the horizontal center of the window in both - // cases. - gravity = Gravity.CENTER_HORIZONTAL; - mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER); - scaleStartCol = scaleCol - gap; - scaleEndCol = scaleCol + gap; - } - break; - case ANCHOR_HORIZONTAL_MODE_RIGHT: - gravity = Gravity.RIGHT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE); - scaleEndCol = scaleCol; - break; - } - switch (verticalMode) { - case ANCHOR_VERTICAL_MODE_TOP: - gravity |= Gravity.TOP; - scaleStartRow = scaleRow; - break; - case ANCHOR_VERTICAL_MODE_CENTER: - gravity |= Gravity.CENTER_VERTICAL; - - // See the above comment. - float gap = Math.min(1 - scaleRow, scaleRow); - scaleStartRow = scaleRow - gap; - scaleEndRow = scaleRow + gap; - break; - case ANCHOR_VERTICAL_MODE_BOTTOM: - gravity |= Gravity.BOTTOM; - scaleEndRow = scaleRow; - break; - } - mCaptionLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout - .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); - setCaptionWindowId(captionWindow.id); - setRowLimit(captionWindow.rowCount); - setGravity(gravity); - setWindowStyle(captionWindow.windowStyle); - if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) { - mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER); - } - if (captionWindow.visible) { - show(); - } else { - hide(); - } - } - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - int width = right - left; - int height = bottom - top; - if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) { - mLastCaptionLayoutWidth = width; - mLastCaptionLayoutHeight = height; - updateTextSize(); - } - } - - private boolean isKoreanLanguageTrack() { - return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null - && mCaptionLayout.getCaptionTrack().language != null - && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0; - } - - private boolean isWideAspectRatio() { - return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null - && mCaptionLayout.getCaptionTrack().wideAspectRatio; - } - - private void updateWidestChar() { - if (isKoreanLanguageTrack()) { - mWidestChar = KOR_ALPHABET; - } else { - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - Charset latin1 = Charset.forName("ISO-8859-1"); - float widestCharWidth = 0f; - for (int i = 0; i < 256; ++i) { - String ch = new String(new byte[]{(byte) i}, latin1); - float charWidth = paint.measureText(ch); - if (widestCharWidth < charWidth) { - widestCharWidth = charWidth; - mWidestChar = ch; - } - } - } - updateTextSize(); - } - - private void updateTextSize() { - if (mCaptionLayout == null) return; - - // Calculate text size based on the max window size. - StringBuilder widestTextBuilder = new StringBuilder(); - int screenColumnCount = getScreenColumnCount(); - for (int i = 0; i < screenColumnCount; ++i) { - widestTextBuilder.append(mWidestChar); - } - String widestText = widestTextBuilder.toString(); - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - float startFontSize = 0f; - float endFontSize = 255f; - Rect boundRect = new Rect(); - while (startFontSize < endFontSize) { - float testTextSize = (startFontSize + endFontSize) / 2f; - paint.setTextSize(testTextSize); - float width = paint.measureText(widestText); - paint.getTextBounds(widestText, 0, widestText.length(), boundRect); - float height = boundRect.height() + width - boundRect.width(); - // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller - // than 1/15 of the height of the safe-title area, and the width shouldn't wider than - // 1/{@code getScreenColumnCount()} of the width of the safe-title area. - if (mCaptionLayout.getWidth() * 0.8f > width - && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) { - startFontSize = testTextSize + 0.01f; - } else { - endFontSize = testTextSize - 0.01f; - } - } - mTextSize = endFontSize * mFontScale; - paint.setTextSize(mTextSize); - float whiteSpaceWidth = paint.measureText(" "); - mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth); - mSubtitleView.setTextSize(mTextSize); - } - - private int getScreenColumnCount() { - float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight(); - boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD; - if (isKoreanLanguageTrack()) { - // Each korean character consumes two slots. - if (isWideAspectRationScreen || isWideAspectRatio()) { - return KR_MAX_COLUMN_COUNT_16_9 / 2; - } else { - return KR_MAX_COLUMN_COUNT_4_3 / 2; - } - } else { - if (isWideAspectRationScreen || isWideAspectRatio()) { - return US_MAX_COLUMN_COUNT_16_9; - } else { - return US_MAX_COLUMN_COUNT_4_3; - } - } - } - - public void removeFromCaptionView() { - if (mCaptionLayout != null) { - mCaptionLayout.removeViewFromSafeTitleArea(this); - mCaptionLayout.removeOnLayoutChangeListener(this); - mCaptionLayout = null; - } - } - - public void setText(String text) { - updateText(text, false); - } - - public void appendText(String text) { - updateText(text, true); - } - - public void clearText() { - mBuilder.clear(); - mSubtitleView.setText(""); - } - - private void updateText(String text, boolean appended) { - if (!appended) { - mBuilder.clear(); - } - if (text != null && text.length() > 0) { - int length = mBuilder.length(); - mBuilder.append(text); - for (CharacterStyle characterStyle : mCharacterStyles) { - mBuilder.setSpan(characterStyle, length, mBuilder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - String[] lines = TextUtils.split(mBuilder.toString(), "\n"); - - // Truncate text not to exceed the row limit. - // Plus one here since the range of the rows is [0, mRowLimit]. - int startRow = Math.max(0, lines.length - (mRowLimit + 1)); - String truncatedText = TextUtils.join("\n", Arrays.copyOfRange( - lines, startRow, lines.length)); - mBuilder.delete(0, mBuilder.length() - truncatedText.length()); - mCurrentTextRow = lines.length - startRow - 1; - - // Trim the buffer first then set text to {@link SubtitleView}. - int start = 0, last = mBuilder.length() - 1; - int end = last; - while ((start <= end) && (mBuilder.charAt(start) <= ' ')) { - ++start; - } - while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') { - --start; - } - while ((end >= start) && (mBuilder.charAt(end) <= ' ')) { - --end; - } - if (start == 0 && end == last) { - mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder)); - mSubtitleView.setText(mBuilder); - } else { - SpannableStringBuilder trim = new SpannableStringBuilder(); - trim.append(mBuilder); - if (end < last) { - trim.delete(end + 1, last + 1); - } - if (start > 0) { - trim.delete(0, start); - } - mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim)); - mSubtitleView.setText(trim); - } - } - - private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) { - ArrayList<Integer> prefixSpaces = new ArrayList<>(); - String[] lines = TextUtils.split(builder.toString(), "\n"); - for (String line : lines) { - int start = 0; - while (start < line.length() && line.charAt(start) <= ' ') { - start++; - } - prefixSpaces.add(start); - } - return prefixSpaces; - } - - public void setRowLimit(int rowLimit) { - if (rowLimit < 0) { - throw new IllegalArgumentException("A rowLimit should have a positive number"); - } - mRowLimit = rowLimit; - } - - private void setWindowStyle(int windowStyle) { - // TODO: Set other attributes of window style. Like fill opacity and fill color. - switch (windowStyle) { - case 2: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 3: - mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 4: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 5: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 6: - mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 7: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM; - break; - default: - if (windowStyle != 0 && windowStyle != 1) { - Log.e(TAG, "Error predefined window style:" + windowStyle); - } - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - } - } -} diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java deleted file mode 100644 index d0f6cf11..00000000 --- a/src/com/android/tv/tuner/cc/Cea708Parser.java +++ /dev/null @@ -1,820 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.cc; - -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; -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.Cea708Data.CcPacket; -import com.android.tv.tuner.util.ByteArrayBuffer; - -import java.io.UnsupportedEncodingException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.TreeSet; - -/** - * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV. - * - * <p>ATSC DTV closed caption data are carried on picture user data of video streams. - * This class starts to parse from picture user data payload, so extraction process of user_data - * from video streams is up to outside of this code. - * - * <p>There are 4 steps to decode user_data to provide closed caption services. - * - * <h3>Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method)</h3> - * - * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a - * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data - * packets must be reassembled in the frame display order, CcPackets are reordered. - * - * <h3>Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method)</h3> - * - * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the - * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet. - * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet - * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has - * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled. - * - * <h3>Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method)</h3> - * - * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption - * track and has a service number, which ranges from 1 to 63, that denotes caption track identity. - * In here, we listen at most one chosen caption track by {@link #mListenServiceNumber}. - * Otherwise, just skip the other service blocks. - * - * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, - * and {@link #parseExt1} methods)</h3> - * - * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of - * ASCII table and consists of specially defined commands and some ASCII control codes which work - * in a behavior slightly different from their original purpose. ASCII control codes and caption - * commands are explicit instructions that control the state of a closed caption service and the - * other ASCII and text codes are implicit instructions that send their characters to buffer. - * - * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the - * same as the range of a byte. - * - * <p>4 main code groups: C0, C1, G0, G1 - * <br>4 extended code groups: C2, C3, G2, G3 - * - * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group - * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while - * {@link #parseExt1} method maps on the extended code groups. - * - * <p>The main code groups: - * <ul> - * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA - * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc, - * even for the alphanumeric characters instead of ASCII characters.</li> - * <li>C1 - contains the caption commands. There are 3 categories of a caption command.</li> - * <ul> - * <li>Window commands: The window commands control a caption window which is addressable area being - * with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)</li> - * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)</li> - * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, RST)</li> - * </ul> - * <li>G0 - same as printable ASCII character set except music note character.</li> - * <li>G1 - same as ISO 8859-1 Latin 1 character set.</li> - * </ul> - * <p>Most of the extended code groups are being skipped. - * - */ -public class Cea708Parser { - private static final String TAG = "Cea708Parser"; - private static final boolean DEBUG = false; - - // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. - private static final int MAX_ALLOCATED_SIZE = 9600 / 8; - private static final String MUSIC_NOTE_CHAR = new String( - "\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - - // The following values are denoting the type of closed caption data. - // See CEA-708B section 4.4.1. - private static final int CC_TYPE_DTVCC_PACKET_START = 3; - private static final int CC_TYPE_DTVCC_PACKET_DATA = 2; - - // The following values are defined in CEA-708B Figure 4 and 6. - private static final int DTVCC_MAX_PACKET_SIZE = 64; - private static final int DTVCC_PACKET_SIZE_SCALE_FACTOR = 2; - private static final int DTVCC_EXTENDED_SERVICE_NUMBER_POINT = 7; - - // The following values are for seeking closed caption tracks. - private static final int DISCOVERY_PERIOD_MS = 10000; // 10 sec - private static final int DISCOVERY_NUM_BYTES_THRESHOLD = 10; // 10 bytes - private static final int DISCOVERY_CC_SERVICE_NUMBER_START = 1; // CC1 - private static final int DISCOVERY_CC_SERVICE_NUMBER_END = 4; // CC4 - - private final ByteArrayBuffer mDtvCcPacket = new ByteArrayBuffer(MAX_ALLOCATED_SIZE); - private final TreeSet<CcPacket> mCcPackets = new TreeSet<>(); - private final StringBuffer mBuffer = new StringBuffer(); - private final SparseIntArray mDiscoveredNumBytes = new SparseIntArray(); // per service number - private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); - private int mCommand = 0; - private int mListenServiceNumber = 0; - private boolean mDtvCcPacking = false; - private boolean mFirstServiceNumberDiscovered; - - // Assign a dummy listener in order to avoid null checks. - private OnCea708ParserListener mListener = new OnCea708ParserListener() { - @Override - public void emitEvent(CaptionEvent event) { - // do nothing - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - // do nothing - } - }; - - /** - * {@link Cea708Parser} emits caption event of three different types. - * {@link OnCea708ParserListener#emitEvent} is invoked with the parameter - * {@link CaptionEvent} to pass all the results to an observer of the decoding process. - * - * <p>{@link CaptionEvent#type} determines the type of the result and - * {@link CaptionEvent#obj} contains the output value of a caption event. - * The observer must do the casting to the corresponding type. - * - * <ul><li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. - * {@code obj} must be of {@link String}.</li> - * - * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a observer. - * {@code obj} must be of {@link Character}.</li> - * - * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. - * {@code obj} must be {@code NULL}.</li></ul> - */ - @IntDef({CAPTION_EMIT_TYPE_BUFFER, CAPTION_EMIT_TYPE_CONTROL, CAPTION_EMIT_TYPE_COMMAND_CWX, - CAPTION_EMIT_TYPE_COMMAND_CLW, CAPTION_EMIT_TYPE_COMMAND_DSW, CAPTION_EMIT_TYPE_COMMAND_HDW, - CAPTION_EMIT_TYPE_COMMAND_TGW, CAPTION_EMIT_TYPE_COMMAND_DLW, CAPTION_EMIT_TYPE_COMMAND_DLY, - CAPTION_EMIT_TYPE_COMMAND_DLC, CAPTION_EMIT_TYPE_COMMAND_RST, CAPTION_EMIT_TYPE_COMMAND_SPA, - CAPTION_EMIT_TYPE_COMMAND_SPC, CAPTION_EMIT_TYPE_COMMAND_SPL, CAPTION_EMIT_TYPE_COMMAND_SWA, - CAPTION_EMIT_TYPE_COMMAND_DFX}) - @Retention(RetentionPolicy.SOURCE) - public @interface CaptionEmitType {} - public static final int CAPTION_EMIT_TYPE_BUFFER = 1; - public static final int CAPTION_EMIT_TYPE_CONTROL = 2; - public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3; - public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4; - public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5; - public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6; - public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10; - public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14; - public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15; - public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16; - - public interface OnCea708ParserListener { - void emitEvent(CaptionEvent event); - void discoverServiceNumber(int serviceNumber); - } - - public void setListener(OnCea708ParserListener listener) { - if (listener != null) { - mListener = listener; - } - } - - public void clear() { - mDtvCcPacket.clear(); - mCcPackets.clear(); - mBuffer.setLength(0); - mDiscoveredNumBytes.clear(); - mCommand = 0; - mDtvCcPacking = false; - } - - public void setListenServiceNumber(int serviceNumber) { - mListenServiceNumber = serviceNumber; - } - - private void emitCaptionEvent(CaptionEvent captionEvent) { - // Emit the existing string buffer before a new event is arrived. - emitCaptionBuffer(); - mListener.emitEvent(captionEvent); - } - - private void emitCaptionBuffer() { - if (mBuffer.length() > 0) { - mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString())); - mBuffer.setLength(0); - } - } - - // Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method) - public void parseClosedCaption(ByteBuffer data, long framePtsUs) { - int ccCount = data.limit() / 3; - byte[] ccBytes = new byte[3 * ccCount]; - for (int i = 0; i < 3 * ccCount; i++) { - ccBytes[i] = data.get(i); - } - CcPacket ccPacket = new CcPacket(ccBytes, ccCount, framePtsUs); - mCcPackets.add(ccPacket); - } - - public boolean processClosedCaptions(long framePtsUs) { - // To get the sorted cc packets that have lower frame pts than current frame pts, - // the following offset divides off the lower side of the packets. - CcPacket offsetPacket = new CcPacket(new byte[0], 0, framePtsUs); - offsetPacket = mCcPackets.lower(offsetPacket); - boolean processed = false; - if (offsetPacket != null) { - while (!mCcPackets.isEmpty() && offsetPacket.compareTo(mCcPackets.first()) >= 0) { - CcPacket packet = mCcPackets.pollFirst(); - parseCcPacket(packet); - processed = true; - } - } - return processed; - } - - // Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method) - private void parseCcPacket(CcPacket ccPacket) { - // For the details of cc packet, see ATSC TSG-676 - Table A8. - byte[] bytes = ccPacket.bytes; - int pos = 0; - for (int i = 0; i < ccPacket.ccCount; ++i) { - boolean ccValid = (bytes[pos] & 0x04) != 0; - int ccType = bytes[pos] & 0x03; - - // The dtvcc should be considered complete: - // - if either ccValid is set and ccType is 3 - // - or ccValid is clear and ccType is 2 or 3. - if (ccValid) { - if (ccType == CC_TYPE_DTVCC_PACKET_START) { - if (mDtvCcPacking) { - parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); - mDtvCcPacket.clear(); - } - mDtvCcPacking = true; - mDtvCcPacket.append(bytes[pos + 1]); - mDtvCcPacket.append(bytes[pos + 2]); - } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) { - mDtvCcPacket.append(bytes[pos + 1]); - mDtvCcPacket.append(bytes[pos + 2]); - } - } else { - if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA) - && mDtvCcPacking) { - mDtvCcPacking = false; - parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); - mDtvCcPacket.clear(); - } - } - pos += 3; - } - } - - // Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method) - private void parseDtvCcPacket(byte[] data, int limit) { - // For the details of DTVCC packet, see CEA-708B Figure 4. - int pos = 0; - int packetSize = data[pos] & 0x3f; - if (packetSize == 0) { - packetSize = DTVCC_MAX_PACKET_SIZE; - } - int calculatedPacketSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR; - if (limit != calculatedPacketSize) { - return; - } - ++pos; - int len = pos + calculatedPacketSize; - while (pos < len) { - // For the details of Service Block, see CEA-708B Figure 5 and 6. - int serviceNumber = (data[pos] & 0xe0) >> 5; - int blockSize = data[pos] & 0x1f; - ++pos; - if (serviceNumber == DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { - serviceNumber = (data[pos] & 0x3f); - ++pos; - - // Return if invalid service number - if (serviceNumber < DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { - return; - } - } - if (pos + blockSize > limit) { - return; - } - - // Send parsed service number in order to find unveiled closed caption tracks which - // are not specified in any ATSC PSIP sections. Since some broadcasts send empty closed - // caption tracks, it detects the proper closed caption tracks by counting the number of - // bytes sent with the same service number during a discovery period. - // The viewer in most TV sets chooses between CC1, CC2, CC3, CC4 to view different - // language captions. Therefore, only CC1, CC2, CC3, CC4 are allowed to be reported. - if (blockSize > 0 && serviceNumber >= DISCOVERY_CC_SERVICE_NUMBER_START - && serviceNumber <= DISCOVERY_CC_SERVICE_NUMBER_END) { - mDiscoveredNumBytes.put( - serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0)); - } - if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime() - || !mFirstServiceNumberDiscovered) { - for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) { - int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i); - if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) { - int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i); - mListener.discoverServiceNumber(discoveredServiceNumber); - mFirstServiceNumberDiscovered = true; - } - } - mDiscoveredNumBytes.clear(); - mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); - } - - // Skip current service block if either there is no block data or the service number - // is not same as listening service number. - if (blockSize == 0 || serviceNumber != mListenServiceNumber) { - pos += blockSize; - continue; - } - - // From this point, starts to read DTVCC coding layer. - // First, identify code groups, which is defined in CEA-708B Section 7.1. - int blockLimit = pos + blockSize; - while (pos < blockLimit) { - pos = parseServiceBlockData(data, pos); - } - - // Emit the buffer after reading codes. - emitCaptionBuffer(); - pos = blockLimit; - } - } - - // Step 4. Main code groups - private int parseServiceBlockData(byte[] data, int pos) { - // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. - mCommand = data[pos] & 0xff; - ++pos; - if (mCommand == Cea708Data.CODE_C0_EXT1) { - pos = parseExt1(data, pos); - } else if (mCommand >= Cea708Data.CODE_C0_RANGE_START - && mCommand <= Cea708Data.CODE_C0_RANGE_END) { - pos = parseC0(data, pos); - } else if (mCommand >= Cea708Data.CODE_C1_RANGE_START - && mCommand <= Cea708Data.CODE_C1_RANGE_END) { - pos = parseC1(data, pos); - } else if (mCommand >= Cea708Data.CODE_G0_RANGE_START - && mCommand <= Cea708Data.CODE_G0_RANGE_END) { - pos = parseG0(data, pos); - } else if (mCommand >= Cea708Data.CODE_G1_RANGE_START - && mCommand <= Cea708Data.CODE_G1_RANGE_END) { - pos = parseG1(data, pos); - } - return pos; - } - - private int parseC0(byte[] data, int pos) { - // For the details of C0 code group, see CEA-708B Section 7.4.1. - // CL Group: C0 Subset of ASCII Control codes - if (mCommand >= Cea708Data.CODE_C0_SKIP2_RANGE_START - && mCommand <= Cea708Data.CODE_C0_SKIP2_RANGE_END) { - if (mCommand == Cea708Data.CODE_C0_P16) { - // TODO : P16 escapes next two bytes for the large character maps.(no standard rule) - // TODO : For korea broadcasting, express whole letters by using this. - try { - if (data[pos] == 0) { - mBuffer.append((char) data[pos + 1]); - } else { - String value = new String( - Arrays.copyOfRange(data, pos, pos + 2), - "EUC-KR"); - mBuffer.append(value); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "P16 Code - Could not find supported encoding", e); - } - } - pos += 2; - } else if (mCommand >= Cea708Data.CODE_C0_SKIP1_RANGE_START - && mCommand <= Cea708Data.CODE_C0_SKIP1_RANGE_END) { - ++pos; - } else { - // NUL, BS, FF, CR interpreted as they are in ASCII control codes. - // HCR moves the pen location to th beginning of the current line and deletes contents. - // FF clears the screen and moves the pen location to (0,0). - // ETX is the NULL command which is used to flush text to the current window when no - // other command is pending. - switch (mCommand) { - case Cea708Data.CODE_C0_NUL: - break; - case Cea708Data.CODE_C0_ETX: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_BS: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_FF: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_CR: - mBuffer.append('\n'); - break; - case Cea708Data.CODE_C0_HCR: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - default: - break; - } - } - return pos; - } - - private int parseC1(byte[] data, int pos) { - // For the details of C1 code group, see CEA-708B Section 8.10. - // CR Group: C1 Caption Control Codes - switch (mCommand) { - case Cea708Data.CODE_C1_CW0: - case Cea708Data.CODE_C1_CW1: - case Cea708Data.CODE_C1_CW2: - case Cea708Data.CODE_C1_CW3: - case Cea708Data.CODE_C1_CW4: - case Cea708Data.CODE_C1_CW5: - case Cea708Data.CODE_C1_CW6: - case Cea708Data.CODE_C1_CW7: { - // SetCurrentWindow0-7 - int windowId = mCommand - Cea708Data.CODE_C1_CW0; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId)); - } - break; - } - - case Cea708Data.CODE_C1_CLW: { - // ClearWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DSW: { - // DisplayWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_HDW: { - // HideWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_TGW: { - // ToggleWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DLW: { - // DeleteWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DLY: { - // Delay - int tenthsOfSeconds = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DLY %d tenths of seconds", - tenthsOfSeconds)); - } - break; - } - case Cea708Data.CODE_C1_DLC: { - // DelayCancel - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null)); - if (DEBUG) { - Log.d(TAG, "CaptionCommand DLC"); - } - break; - } - - case Cea708Data.CODE_C1_RST: { - // Reset - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null)); - if (DEBUG) { - Log.d(TAG, "CaptionCommand RST"); - } - break; - } - - case Cea708Data.CODE_C1_SPA: { - // SetPenAttributes - int textTag = (data[pos] & 0xf0) >> 4; - int penSize = data[pos] & 0x03; - int penOffset = (data[pos] & 0x0c) >> 2; - boolean italic = (data[pos + 1] & 0x80) != 0; - boolean underline = (data[pos + 1] & 0x40) != 0; - int edgeType = (data[pos + 1] & 0x38) >> 3; - int fontTag = data[pos + 1] & 0x7; - pos += 2; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPA, - new CaptionPenAttr(penSize, penOffset, textTag, fontTag, edgeType, - underline, italic))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, " - + "fontTag: %d, edgeType: %d, underline: %s, italic: %s", - penSize, penOffset, textTag, fontTag, edgeType, underline, italic)); - } - break; - } - - case Cea708Data.CODE_C1_SPC: { - // SetPenColor - int opacity = (data[pos] & 0xc0) >> 6; - int red = (data[pos] & 0x30) >> 4; - int green = (data[pos] & 0x0c) >> 2; - int blue = data[pos] & 0x03; - CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue); - ++pos; - opacity = (data[pos] & 0xc0) >> 6; - red = (data[pos] & 0x30) >> 4; - green = (data[pos] & 0x0c) >> 2; - blue = data[pos] & 0x03; - CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue); - ++pos; - red = (data[pos] & 0x30) >> 4; - green = (data[pos] & 0x0c) >> 2; - blue = data[pos] & 0x03; - CaptionColor edgeColor = new CaptionColor( - CaptionColor.OPACITY_SOLID, red, green, blue); - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPC, - new CaptionPenColor(foregroundColor, backgroundColor, edgeColor))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s", - foregroundColor, backgroundColor, edgeColor)); - } - break; - } - - case Cea708Data.CODE_C1_SPL: { - // SetPenLocation - // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats - int row = data[pos] & 0x0f; - int column = data[pos + 1] & 0x3f; - pos += 2; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPL, - new CaptionPenLocation(row, column))); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand SPL row: %d, column: %d", - row, column)); - } - break; - } - - case Cea708Data.CODE_C1_SWA: { - // SetWindowAttributes - int opacity = (data[pos] & 0xc0) >> 6; - int red = (data[pos] & 0x30) >> 4; - int green = (data[pos] & 0x0c) >> 2; - int blue = data[pos] & 0x03; - CaptionColor fillColor = new CaptionColor(opacity, red, green, blue); - int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5; - red = (data[pos + 1] & 0x30) >> 4; - green = (data[pos + 1] & 0x0c) >> 2; - blue = data[pos + 1] & 0x03; - CaptionColor borderColor = new CaptionColor( - CaptionColor.OPACITY_SOLID, red, green, blue); - boolean wordWrap = (data[pos + 2] & 0x40) != 0; - int printDirection = (data[pos + 2] & 0x30) >> 4; - int scrollDirection = (data[pos + 2] & 0x0c) >> 2; - int justify = (data[pos + 2] & 0x03); - int effectSpeed = (data[pos + 3] & 0xf0) >> 4; - int effectDirection = (data[pos + 3] & 0x0c) >> 2; - int displayEffect = data[pos + 3] & 0x3; - pos += 4; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SWA, - new CaptionWindowAttr(fillColor, borderColor, borderType, wordWrap, - printDirection, scrollDirection, justify, - effectDirection, effectSpeed, displayEffect))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d" - + "wordWrap: %s, printDirection: %d, scrollDirection: %d, " - + "justify: %s, effectDirection: %d, effectSpeed: %d, " - + "displayEffect: %d", - fillColor, borderColor, borderType, wordWrap, printDirection, - scrollDirection, justify, effectDirection, effectSpeed, displayEffect)); - } - break; - } - - case Cea708Data.CODE_C1_DF0: - case Cea708Data.CODE_C1_DF1: - case Cea708Data.CODE_C1_DF2: - case Cea708Data.CODE_C1_DF3: - case Cea708Data.CODE_C1_DF4: - case Cea708Data.CODE_C1_DF5: - case Cea708Data.CODE_C1_DF6: - case Cea708Data.CODE_C1_DF7: { - // DefineWindow0-7 - int windowId = mCommand - Cea708Data.CODE_C1_DF0; - boolean visible = (data[pos] & 0x20) != 0; - boolean rowLock = (data[pos] & 0x10) != 0; - boolean columnLock = (data[pos] & 0x08) != 0; - int priority = data[pos] & 0x07; - boolean relativePositioning = (data[pos + 1] & 0x80) != 0; - int anchorVertical = data[pos + 1] & 0x7f; - int anchorHorizontal = data[pos + 2] & 0xff; - int anchorId = (data[pos + 3] & 0xf0) >> 4; - int rowCount = data[pos + 3] & 0x0f; - int columnCount = data[pos + 4] & 0x3f; - int windowStyle = (data[pos + 5] & 0x38) >> 3; - int penStyle = data[pos + 5] & 0x07; - pos += 6; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DFX, - new CaptionWindow(windowId, visible, rowLock, columnLock, priority, - relativePositioning, anchorVertical, anchorHorizontal, anchorId, - rowCount, columnCount, penStyle, windowStyle))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, " - + "rowLock: %s, visible: %s, anchorVertical: %d, " - + "relativePositioning: %s, anchorHorizontal: %d, " - + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, " - + "windowStyle: %d", - windowId, priority, columnLock, rowLock, visible, anchorVertical, - relativePositioning, anchorHorizontal, rowCount, anchorId, columnCount, - penStyle, windowStyle)); - } - break; - } - - default: - break; - } - return pos; - } - - private int parseG0(byte[] data, int pos) { - // For the details of G0 code group, see CEA-708B Section 7.4.3. - // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII) - if (mCommand == Cea708Data.CODE_G0_MUSICNOTE) { - // Music note. - mBuffer.append(MUSIC_NOTE_CHAR); - } else { - // Put ASCII code into buffer. - mBuffer.append((char) mCommand); - } - return pos; - } - - private int parseG1(byte[] data, int pos) { - // For the details of G0 code group, see CEA-708B Section 7.4.4. - // GR Group: G1 ISO 8859-1 Latin 1 Characters - // Put ASCII Extended character set into buffer. - mBuffer.append((char) mCommand); - return pos; - } - - // Step 4. Extended code groups - private int parseExt1(byte[] data, int pos) { - // For the details of EXT1 code group, see CEA-708B Section 7.2. - mCommand = data[pos] & 0xff; - ++pos; - if (mCommand >= Cea708Data.CODE_C2_RANGE_START - && mCommand <= Cea708Data.CODE_C2_RANGE_END) { - pos = parseC2(data, pos); - } else if (mCommand >= Cea708Data.CODE_C3_RANGE_START - && mCommand <= Cea708Data.CODE_C3_RANGE_END) { - pos = parseC3(data, pos); - } else if (mCommand >= Cea708Data.CODE_G2_RANGE_START - && mCommand <= Cea708Data.CODE_G2_RANGE_END) { - pos = parseG2(data, pos); - } else if (mCommand >= Cea708Data.CODE_G3_RANGE_START - && mCommand <= Cea708Data.CODE_G3_RANGE_END) { - pos = parseG3(data ,pos); - } - return pos; - } - - private int parseC2(byte[] data, int pos) { - // For the details of C2 code group, see CEA-708B Section 7.4.7. - // Extended Miscellaneous Control Codes - // C2 Table : No commands as of CEA-708B. A decoder must skip. - if (mCommand >= Cea708Data.CODE_C2_SKIP0_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP0_RANGE_END) { - // Do nothing. - } else if (mCommand >= Cea708Data.CODE_C2_SKIP1_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP1_RANGE_END) { - ++pos; - } else if (mCommand >= Cea708Data.CODE_C2_SKIP2_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP2_RANGE_END) { - pos += 2; - } else if (mCommand >= Cea708Data.CODE_C2_SKIP3_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP3_RANGE_END) { - pos += 3; - } - return pos; - } - - private int parseC3(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.8. - // Extended Control Code Set 2 - // C3 Table : No commands as of CEA-708B. A decoder must skip. - if (mCommand >= Cea708Data.CODE_C3_SKIP4_RANGE_START - && mCommand <= Cea708Data.CODE_C3_SKIP4_RANGE_END) { - pos += 4; - } else if (mCommand >= Cea708Data.CODE_C3_SKIP5_RANGE_START - && mCommand <= Cea708Data.CODE_C3_SKIP5_RANGE_END) { - pos += 5; - } - return pos; - } - - private int parseG2(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.5. - // Extended Control Code Set 1(G2 Table) - switch (mCommand) { - case Cea708Data.CODE_G2_TSP: - // TODO : TSP is the Transparent space - break; - case Cea708Data.CODE_G2_NBTSP: - // TODO : NBTSP is Non-Breaking Transparent Space. - break; - case Cea708Data.CODE_G2_BLK: - // TODO : BLK indicates a solid block which fills the entire character block - // TODO : with a solid foreground color. - break; - default: - break; - } - return pos; - } - - private int parseG3(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.6. - // Future characters and icons(G3 Table) - if (mCommand == Cea708Data.CODE_G3_CC) { - // TODO : [CC] icon with square corners - } - - // Do nothing - return pos; - } -} diff --git a/src/com/android/tv/tuner/data/Cea708Data.java b/src/com/android/tv/tuner/data/Cea708Data.java deleted file mode 100644 index 6350d63c..00000000 --- a/src/com/android/tv/tuner/data/Cea708Data.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.data; - -import com.android.tv.tuner.cc.Cea708Parser; - -import android.graphics.Color; -import android.support.annotation.NonNull; - -/** - * Collection of CEA-708 structures. - */ -public class Cea708Data { - - private Cea708Data() { - } - - // According to CEA-708B, the range of valid service number is between 1 and 63. - public static final int EMPTY_SERVICE_NUMBER = 0; - - // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. - public static final int CODE_C0_RANGE_START = 0x00; - public static final int CODE_C0_RANGE_END = 0x1f; - public static final int CODE_C1_RANGE_START = 0x80; - public static final int CODE_C1_RANGE_END = 0x9f; - public static final int CODE_G0_RANGE_START = 0x20; - public static final int CODE_G0_RANGE_END = 0x7f; - public static final int CODE_G1_RANGE_START = 0xa0; - public static final int CODE_G1_RANGE_END = 0xff; - public static final int CODE_C2_RANGE_START = 0x00; - public static final int CODE_C2_RANGE_END = 0x1f; - public static final int CODE_C3_RANGE_START = 0x80; - public static final int CODE_C3_RANGE_END = 0x9f; - public static final int CODE_G2_RANGE_START = 0x20; - public static final int CODE_G2_RANGE_END = 0x7f; - public static final int CODE_G3_RANGE_START = 0xa0; - public static final int CODE_G3_RANGE_END = 0xff; - - // The following ranges are defined in CEA-708B Section 7.4.1. - public static final int CODE_C0_SKIP2_RANGE_START = 0x18; - public static final int CODE_C0_SKIP2_RANGE_END = 0x1f; - public static final int CODE_C0_SKIP1_RANGE_START = 0x10; - public static final int CODE_C0_SKIP1_RANGE_END = 0x17; - - // The following ranges are defined in CEA-708B Section 7.4.7. - public static final int CODE_C2_SKIP0_RANGE_START = 0x00; - public static final int CODE_C2_SKIP0_RANGE_END = 0x07; - public static final int CODE_C2_SKIP1_RANGE_START = 0x08; - public static final int CODE_C2_SKIP1_RANGE_END = 0x0f; - public static final int CODE_C2_SKIP2_RANGE_START = 0x10; - public static final int CODE_C2_SKIP2_RANGE_END = 0x17; - public static final int CODE_C2_SKIP3_RANGE_START = 0x18; - public static final int CODE_C2_SKIP3_RANGE_END = 0x1f; - - // The following ranges are defined in CEA-708B Section 7.4.8. - public static final int CODE_C3_SKIP4_RANGE_START = 0x80; - public static final int CODE_C3_SKIP4_RANGE_END = 0x87; - public static final int CODE_C3_SKIP5_RANGE_START = 0x88; - public static final int CODE_C3_SKIP5_RANGE_END = 0x8f; - - // The following values are the special characters of CEA-708 spec. - public static final int CODE_C0_NUL = 0x00; - public static final int CODE_C0_ETX = 0x03; - public static final int CODE_C0_BS = 0x08; - public static final int CODE_C0_FF = 0x0c; - public static final int CODE_C0_CR = 0x0d; - public static final int CODE_C0_HCR = 0x0e; - public static final int CODE_C0_EXT1 = 0x10; - public static final int CODE_C0_P16 = 0x18; - public static final int CODE_G0_MUSICNOTE = 0x7f; - public static final int CODE_G2_TSP = 0x20; - public static final int CODE_G2_NBTSP = 0x21; - public static final int CODE_G2_BLK = 0x30; - public static final int CODE_G3_CC = 0xa0; - - // The following values are the command bits of CEA-708 spec. - public static final int CODE_C1_CW0 = 0x80; - public static final int CODE_C1_CW1 = 0x81; - public static final int CODE_C1_CW2 = 0x82; - public static final int CODE_C1_CW3 = 0x83; - public static final int CODE_C1_CW4 = 0x84; - public static final int CODE_C1_CW5 = 0x85; - public static final int CODE_C1_CW6 = 0x86; - public static final int CODE_C1_CW7 = 0x87; - public static final int CODE_C1_CLW = 0x88; - public static final int CODE_C1_DSW = 0x89; - public static final int CODE_C1_HDW = 0x8a; - public static final int CODE_C1_TGW = 0x8b; - public static final int CODE_C1_DLW = 0x8c; - public static final int CODE_C1_DLY = 0x8d; - public static final int CODE_C1_DLC = 0x8e; - public static final int CODE_C1_RST = 0x8f; - public static final int CODE_C1_SPA = 0x90; - public static final int CODE_C1_SPC = 0x91; - public static final int CODE_C1_SPL = 0x92; - public static final int CODE_C1_SWA = 0x97; - public static final int CODE_C1_DF0 = 0x98; - public static final int CODE_C1_DF1 = 0x99; - public static final int CODE_C1_DF2 = 0x9a; - public static final int CODE_C1_DF3 = 0x9b; - public static final int CODE_C1_DF4 = 0x9c; - public static final int CODE_C1_DF5 = 0x9d; - public static final int CODE_C1_DF6 = 0x9e; - public static final int CODE_C1_DF7 = 0x9f; - - public static class CcPacket implements Comparable<CcPacket> { - public final byte[] bytes; - public final int ccCount; - public final long pts; - - public CcPacket(byte[] bytes, int ccCount, long pts) { - this.bytes = bytes; - this.ccCount = ccCount; - this.pts = pts; - } - - @Override - public int compareTo(@NonNull CcPacket another) { - return Long.compare(pts, another.pts); - } - } - - /** - * CEA-708B-specific color. - */ - public static class CaptionColor { - public static final int OPACITY_SOLID = 0; - public static final int OPACITY_FLASH = 1; - public static final int OPACITY_TRANSLUCENT = 2; - public static final int OPACITY_TRANSPARENT = 3; - - private static final int[] COLOR_MAP = new int[] { 0x00, 0x0f, 0xf0, 0xff }; - private static final int[] OPACITY_MAP = new int[] { 0xff, 0xfe, 0x80, 0x00 }; - - public final int opacity; - public final int red; - public final int green; - public final int blue; - - public CaptionColor(int opacity, int red, int green, int blue) { - this.opacity = opacity; - this.red = red; - this.green = green; - this.blue = blue; - } - - public int getArgbValue() { - return Color.argb( - OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]); - } - } - - /** - * Caption event generated by {@link Cea708Parser}. - */ - public static class CaptionEvent { - @Cea708Parser.CaptionEmitType public final int type; - public final Object obj; - - public CaptionEvent(int type, Object obj) { - this.type = type; - this.obj = obj; - } - } - - /** - * Pen style information. - */ - public static class CaptionPenAttr { - // Pen sizes - public static final int PEN_SIZE_SMALL = 0; - public static final int PEN_SIZE_STANDARD = 1; - public static final int PEN_SIZE_LARGE = 2; - - // Offsets - public static final int OFFSET_SUBSCRIPT = 0; - public static final int OFFSET_NORMAL = 1; - public static final int OFFSET_SUPERSCRIPT = 2; - - public final int penSize; - public final int penOffset; - public final int textTag; - public final int fontTag; - public final int edgeType; - public final boolean underline; - public final boolean italic; - - public CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, - boolean underline, boolean italic) { - this.penSize = penSize; - this.penOffset = penOffset; - this.textTag = textTag; - this.fontTag = fontTag; - this.edgeType = edgeType; - this.underline = underline; - this.italic = italic; - } - } - - /** - * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a - * pen. - */ - public static class CaptionPenColor { - public final CaptionColor foregroundColor; - public final CaptionColor backgroundColor; - public final CaptionColor edgeColor; - - public CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, - CaptionColor edgeColor) { - this.foregroundColor = foregroundColor; - this.backgroundColor = backgroundColor; - this.edgeColor = edgeColor; - } - } - - /** - * Location information of a pen. - */ - public static class CaptionPenLocation { - public final int row; - public final int column; - - public CaptionPenLocation(int row, int column) { - this.row = row; - this.column = column; - } - } - - /** - * Attributes of a caption window, which is defined in CEA-708B. - */ - public static class CaptionWindowAttr { - public static final int JUSTIFY_LEFT = 0; - public static final int JUSTIFY_CENTER = 2; - public static final int PRINT_LEFT_TO_RIGHT = 0; - public static final int PRINT_RIGHT_TO_LEFT = 1; - public static final int PRINT_TOP_TO_BOTTOM = 2; - public static final int PRINT_BOTTOM_TO_TOP = 3; - - public final CaptionColor fillColor; - public final CaptionColor borderColor; - public final int borderType; - public final boolean wordWrap; - public final int printDirection; - public final int scrollDirection; - public final int justify; - public final int effectDirection; - public final int effectSpeed; - public final int displayEffect; - - public CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, - boolean wordWrap, int printDirection, int scrollDirection, int justify, - int effectDirection, - int effectSpeed, int displayEffect) { - this.fillColor = fillColor; - this.borderColor = borderColor; - this.borderType = borderType; - this.wordWrap = wordWrap; - this.printDirection = printDirection; - this.scrollDirection = scrollDirection; - this.justify = justify; - this.effectDirection = effectDirection; - this.effectSpeed = effectSpeed; - this.displayEffect = displayEffect; - } - } - - /** - * Construction information of the caption window of CEA-708B. - */ - public static class CaptionWindow { - public final int id; - public final boolean visible; - public final boolean rowLock; - public final boolean columnLock; - public final int priority; - public final boolean relativePositioning; - public final int anchorVertical; - public final int anchorHorizontal; - public final int anchorId; - public final int rowCount; - public final int columnCount; - public final int penStyle; - public final int windowStyle; - - public CaptionWindow(int id, boolean visible, - boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, - int anchorVertical, int anchorHorizontal, int anchorId, - int rowCount, int columnCount, int penStyle, int windowStyle) { - this.id = id; - this.visible = visible; - this.rowLock = rowLock; - this.columnLock = columnLock; - this.priority = priority; - this.relativePositioning = relativePositioning; - this.anchorVertical = anchorVertical; - this.anchorHorizontal = anchorHorizontal; - this.anchorId = anchorId; - this.rowCount = rowCount; - this.columnCount = columnCount; - this.penStyle = penStyle; - this.windowStyle = windowStyle; - } - } -} diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java deleted file mode 100644 index 67700c6a..00000000 --- a/src/com/android/tv/tuner/data/PsiData.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.android.tv.tuner.data; - -import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; - -import java.util.List; - -/** - * Collection of MPEG PSI table items. - */ -public class PsiData { - - private PsiData() { - } - - public static class PatItem { - private final int mProgramNo; - private final int mPmtPid; - - public PatItem(int programNo, int pmtPid) { - mProgramNo = programNo; - mPmtPid = pmtPid; - } - - public int getProgramNo() { - return mProgramNo; - } - - public int getPmtPid() { - return mPmtPid; - } - - @Override - public String toString() { - return String.format("Program No: %x PMT Pid: %x", mProgramNo, mPmtPid); - } - } - - public static class PmtItem { - public static final int ES_PID_PCR = 0x100; - - private final int mStreamType; - private final int mEsPid; - private final List<AtscAudioTrack> mAudioTracks; - private final List<AtscCaptionTrack> mCaptionTracks; - - public PmtItem(int streamType, int esPid, - List<AtscAudioTrack> audioTracks, List<AtscCaptionTrack> captionTracks) { - mStreamType = streamType; - mEsPid = esPid; - mAudioTracks = audioTracks; - mCaptionTracks = captionTracks; - } - - public int getStreamType() { - return mStreamType; - } - - public int getEsPid() { - return mEsPid; - } - - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - - @Override - public String toString() { - return String.format("Stream Type: %x ES Pid: %x AudioTracks: %s CaptionTracks: %s", - mStreamType, mEsPid, mAudioTracks, mCaptionTracks); - } - } -} diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java deleted file mode 100644 index 8f98e67c..00000000 --- a/src/com/android/tv/tuner/data/PsipData.java +++ /dev/null @@ -1,820 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.data; - -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.text.format.DateUtils; - -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 com.android.tv.util.StringUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; - -/** - * Collection of ATSC PSIP table items. - */ -public class PsipData { - - private PsipData() { - } - - public static class PsipSection { - private final int mTableId; - private final int mTableIdExtension; - private final int mSectionNumber; - private final boolean mCurrentNextIndicator; - - public static PsipSection create(byte[] data) { - if (data.length < 9) { - return null; - } - int tableId = data[0] & 0xff; - int tableIdExtension = (data[3] & 0xff) << 8 | (data[4] & 0xff); - int sectionNumber = data[6] & 0xff; - boolean currentNextIndicator = (data[5] & 0x01) != 0; - return new PsipSection(tableId, tableIdExtension, sectionNumber, currentNextIndicator); - } - - private PsipSection(int tableId, int tableIdExtension, int sectionNumber, - boolean currentNextIndicator) { - mTableId = tableId; - mTableIdExtension = tableIdExtension; - mSectionNumber = sectionNumber; - mCurrentNextIndicator = currentNextIndicator; - } - - public int getTableId() { - return mTableId; - } - - public int getTableIdExtension() { - return mTableIdExtension; - } - - public int getSectionNumber() { - return mSectionNumber; - } - - // This is for indicating that the section sent is applicable. - // We only consider a situation where currentNextIndicator is expected to have a true value. - // So, we are not going to compare this variable in hashCode() and equals() methods. - public boolean getCurrentNextIndicator() { - return mCurrentNextIndicator; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + mTableId; - result = 31 * result + mTableIdExtension; - result = 31 * result + mSectionNumber; - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof PsipSection) { - PsipSection another = (PsipSection) obj; - return mTableId == another.getTableId() - && mTableIdExtension == another.getTableIdExtension() - && mSectionNumber == another.getSectionNumber(); - } - return false; - } - } - - /** - * {@link TvTracksInterface} for serving the audio and caption tracks. - */ - public interface TvTracksInterface { - /** - * Set the flag that tells the caption tracks have been found in this section container. - */ - void setHasCaptionTrack(); - - /** - * Returns whether or not the caption tracks have been found in this section container. - * If true, zero caption track will be interpreted as a clearance of the caption tracks. - */ - boolean hasCaptionTrack(); - - /** - * Returns the audio tracks received. - */ - List<AtscAudioTrack> getAudioTracks(); - - /** - * Returns the caption tracks received. - */ - List<AtscCaptionTrack> getCaptionTracks(); - } - - public static class MgtItem { - public static final int TABLE_TYPE_EIT_RANGE_START = 0x0100; - public static final int TABLE_TYPE_EIT_RANGE_END = 0x017f; - public static final int TABLE_TYPE_CHANNEL_ETT = 0x0004; - public static final int TABLE_TYPE_ETT_RANGE_START = 0x0200; - public static final int TABLE_TYPE_ETT_RANGE_END = 0x027f; - - private final int mTableType; - private final int mTableTypePid; - - public MgtItem(int tableType, int tableTypePid) { - mTableType = tableType; - mTableTypePid = tableTypePid; - } - - public int getTableType() { - return mTableType; - } - - public int getTableTypePid() { - return mTableTypePid; - } - } - - public static class VctItem { - private final String mShortName; - private final String mLongName; - private final int mServiceType; - private final int mChannelTsid; - private final int mProgramNumber; - private final int mMajorChannelNumber; - private final int mMinorChannelNumber; - private final int mSourceId; - private String mDescription; - - public VctItem(String shortName, String longName, int serviceType, int channelTsid, - int programNumber, int majorChannelNumber, int minorChannelNumber, int sourceId) { - mShortName = shortName; - mLongName = longName; - mServiceType = serviceType; - mChannelTsid = channelTsid; - mProgramNumber = programNumber; - mMajorChannelNumber = majorChannelNumber; - mMinorChannelNumber = minorChannelNumber; - mSourceId = sourceId; - } - - public String getShortName() { - return mShortName; - } - - public String getLongName() { - return mLongName; - } - - public int getServiceType() { - return mServiceType; - } - - public int getChannelTsid() { - return mChannelTsid; - } - - public int getProgramNumber() { - return mProgramNumber; - } - - public int getMajorChannelNumber() { - return mMajorChannelNumber; - } - - public int getMinorChannelNumber() { - return mMinorChannelNumber; - } - - public int getSourceId() { - return mSourceId; - } - - @Override - public String toString() { - return String - .format(Locale.US, "ShortName: %s LongName: %s ServiceType: %d ChannelTsid: %x " - + "ProgramNumber:%d %d-%d SourceId: %x", - mShortName, mLongName, mServiceType, mChannelTsid, - mProgramNumber, mMajorChannelNumber, mMinorChannelNumber, mSourceId); - } - - public void setDescription(String description) { - mDescription = description; - } - - public String getDescription() { - return mDescription; - } - } - - public static class SdtItem { - private final String mServiceName; - private final String mServiceProviderName; - private final int mServiceType; - private final int mServiceId; - private final int mOriginalNetWorkId; - - public SdtItem(String serviceName, String serviceProviderName, int serviceType, - int serviceId, int originalNetWorkId) { - mServiceName = serviceName; - mServiceProviderName = serviceProviderName; - mServiceType = serviceType; - mServiceId = serviceId; - mOriginalNetWorkId = originalNetWorkId; - } - - public String getServiceName() { - return mServiceName; - } - - public String getServiceProviderName() { - return mServiceProviderName; - } - - public int getServiceType() { - return mServiceType; - } - - public int getServiceId() { - return mServiceId; - } - - public int getOriginalNetworkId() { - return mOriginalNetWorkId; - } - - @Override - public String toString() { - return String.format("ServiceName: %s ServiceProviderName:%s ServiceType:%d " - + "OriginalNetworkId:%d", - mServiceName, mServiceProviderName, mServiceType, mOriginalNetWorkId); - } - } - - /** - * A base class for descriptors of Ts packets. - */ - public abstract static class TsDescriptor { - public abstract int getTag(); - } - - public static class ContentAdvisoryDescriptor extends TsDescriptor { - private final List<RatingRegion> mRatingRegions; - - public ContentAdvisoryDescriptor(List<RatingRegion> ratingRegions) { - mRatingRegions = ratingRegions; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_CONTENT_ADVISORY; - } - - public List<RatingRegion> getRatingRegions() { - return mRatingRegions; - } - } - - public static class CaptionServiceDescriptor extends TsDescriptor { - private final List<AtscCaptionTrack> mCaptionTracks; - - public CaptionServiceDescriptor(List<AtscCaptionTrack> captionTracks) { - mCaptionTracks = captionTracks; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_CAPTION_SERVICE; - } - - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - } - - public static class ExtendedChannelNameDescriptor extends TsDescriptor { - private final String mLongChannelName; - - public ExtendedChannelNameDescriptor(String longChannelName) { - mLongChannelName = longChannelName; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME; - } - - public String getLongChannelName() { - return mLongChannelName; - } - } - - public static class GenreDescriptor extends TsDescriptor { - private final String[] mBroadcastGenres; - private final String[] mCanonicalGenres; - - public GenreDescriptor(String[] broadcastGenres, String[] canonicalGenres) { - mBroadcastGenres = broadcastGenres; - mCanonicalGenres = canonicalGenres; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_GENRE; - } - - public String[] getBroadcastGenres() { - return mBroadcastGenres; - } - - public String[] getCanonicalGenres() { - return mCanonicalGenres; - } - } - - public static class Ac3AudioDescriptor extends TsDescriptor { - // See A/52 Annex A. Table A4.2 - private static final byte SAMPLE_RATE_CODE_48000HZ = 0; - private static final byte SAMPLE_RATE_CODE_44100HZ = 1; - private static final byte SAMPLE_RATE_CODE_32000HZ = 2; - - private final byte mSampleRateCode; - private final byte mBsid; - private final byte mBitRateCode; - private final byte mSurroundMode; - private final byte mBsmod; - private final int mNumChannels; - private final boolean mFullSvc; - private final byte mLangCod; - private final byte mLangCod2; - private final byte mMainId; - private final byte mPriority; - private final byte mAsvcflags; - private final String mText; - private final String mLanguage; - private final String mLanguage2; - - public Ac3AudioDescriptor(byte sampleRateCode, byte bsid, byte bitRateCode, - byte surroundMode, byte bsmod, int numChannels, boolean fullSvc, byte langCod, - byte langCod2, byte mainId, byte priority, byte asvcflags, String text, - String language, String language2) { - mSampleRateCode = sampleRateCode; - mBsid = bsid; - mBitRateCode = bitRateCode; - mSurroundMode = surroundMode; - mBsmod = bsmod; - mNumChannels = numChannels; - mFullSvc = fullSvc; - mLangCod = langCod; - mLangCod2 = langCod2; - mMainId = mainId; - mPriority = priority; - mAsvcflags = asvcflags; - mText = text; - mLanguage = language; - mLanguage2 = language2; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_AC3_AUDIO_STREAM; - } - - public byte getSampleRateCode() { - return mSampleRateCode; - } - - public int getSampleRate() { - switch (mSampleRateCode) { - case SAMPLE_RATE_CODE_48000HZ: - return 48000; - case SAMPLE_RATE_CODE_44100HZ: - return 44100; - case SAMPLE_RATE_CODE_32000HZ: - return 32000; - default: - return 0; - } - } - - public byte getBsid() { - return mBsid; - } - - public byte getBitRateCode() { - return mBitRateCode; - } - - public byte getSurroundMode() { - return mSurroundMode; - } - - public byte getBsmod() { - return mBsmod; - } - - public int getNumChannels() { - return mNumChannels; - } - - public boolean isFullSvc() { - return mFullSvc; - } - - public byte getLangCod() { - return mLangCod; - } - - public byte getLangCod2() { - return mLangCod2; - } - - public byte getMainId() { - return mMainId; - } - - public byte getPriority() { - return mPriority; - } - - public byte getAsvcflags() { - return mAsvcflags; - } - - public String getText() { - return mText; - } - - public String getLanguage() { - return mLanguage; - } - - public String getLanguage2() { - return mLanguage2; - } - - @Override - public String toString() { - return String.format(Locale.US, - "AC3 audio stream sampleRateCode: %d, bsid: %d, bitRateCode: %d, " - + "surroundMode: %d, bsmod: %d, numChannels: %d, fullSvc: %s, langCod: %d, " - + "langCod2: %d, mainId: %d, priority: %d, avcflags: %d, text: %s, language: %s" - + ", language2: %s", mSampleRateCode, mBsid, mBitRateCode, mSurroundMode, - mBsmod, mNumChannels, mFullSvc, mLangCod, mLangCod2, mMainId, mPriority, - mAsvcflags, mText, mLanguage, mLanguage2); - } - } - - public static class Iso639LanguageDescriptor extends TsDescriptor { - private final List<AtscAudioTrack> mAudioTracks; - - public Iso639LanguageDescriptor(List<AtscAudioTrack> audioTracks) { - mAudioTracks = audioTracks; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_ISO639LANGUAGE; - } - - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - @Override - public String toString() { - return String.format("%s %s", getClass().getName(), mAudioTracks); - } - } - - public static class ServiceDescriptor extends TsDescriptor { - private final int mServiceType; - private final String mServiceProviderName; - private final String mServiceName; - - public ServiceDescriptor(int serviceType, String serviceProviderName, String serviceName) { - mServiceType = serviceType; - mServiceProviderName = serviceProviderName; - mServiceName = serviceName; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_SERVICE; - } - - public int getServiceType() { - return mServiceType; - } - - public String getServiceProviderName() { - return mServiceProviderName; - } - - public String getServiceName() { - return mServiceName; - } - - @Override - public String toString() { - return String.format( - "Service descriptor, service type: %d, " - + "service provider name: %s, " - + "service name: %s", mServiceType, mServiceProviderName, mServiceName); - } - } - - public static class ShortEventDescriptor extends TsDescriptor { - private final String mLanguage; - private final String mEventName; - private final String mText; - - public ShortEventDescriptor(String language, String eventName, String text) { - mLanguage = language; - mEventName = eventName; - mText = text; - } - - public String getEventName() { - return mEventName; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_SHORT_EVENT; - } - - @Override - public String toString() { - return String.format("ShortEvent Descriptor, language:%s, event name: %s, " - + "text:%s", mLanguage, mEventName, mText); - } - } - - public static class ParentalRatingDescriptor extends TsDescriptor { - private final HashMap<String, Integer> mRatings; - - public ParentalRatingDescriptor(HashMap<String, Integer> ratings) { - mRatings = ratings; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_PARENTAL_RATING; - } - - public HashMap<String, Integer> getRatings() { - return mRatings; - } - - @Override - public String toString() { - return String.format("Parental rating descriptor, ratings:" + mRatings); - } - } - - public static class RatingRegion { - private final int mName; - private final String mDescription; - private final List<RegionalRating> mRegionalRatings; - - public RatingRegion(int name, String description, List<RegionalRating> regionalRatings) { - mName = name; - mDescription = description; - mRegionalRatings = regionalRatings; - } - - public int getName() { - return mName; - } - - public String getDescription() { - return mDescription; - } - - public List<RegionalRating> getRegionalRatings() { - return mRegionalRatings; - } - } - - public static class RegionalRating { - private final int mDimension; - private final int mRating; - - public RegionalRating(int dimension, int rating) { - mDimension = dimension; - mRating = rating; - } - - public int getDimension() { - return mDimension; - } - - public int getRating() { - return mRating; - } - } - - public static class EitItem implements Comparable<EitItem>, TvTracksInterface { - public static final long INVALID_PROGRAM_ID = -1; - - // A program id is a primary key of TvContract.Programs table. So it must be positive. - private final long mProgramId; - private final int mEventId; - private final String mTitleText; - private String mDescription; - private final long mStartTime; - private final int mLengthInSecond; - private final String mContentRating; - private final List<AtscAudioTrack> mAudioTracks; - private final List<AtscCaptionTrack> mCaptionTracks; - private boolean mHasCaptionTrack; - private final String mBroadcastGenre; - private final String mCanonicalGenre; - - public EitItem(long programId, int eventId, String titleText, long startTime, - int lengthInSecond, String contentRating, List<AtscAudioTrack> audioTracks, - List<AtscCaptionTrack> captionTracks, String broadcastGenre, String canonicalGenre, - String description) { - mProgramId = programId; - mEventId = eventId; - mTitleText = titleText; - mStartTime = startTime; - mLengthInSecond = lengthInSecond; - mContentRating = contentRating; - mAudioTracks = audioTracks; - mCaptionTracks = captionTracks; - mBroadcastGenre = broadcastGenre; - mCanonicalGenre = canonicalGenre; - mDescription = description; - } - - public long getProgramId() { - return mProgramId; - } - - public int getEventId() { - return mEventId; - } - - public String getTitleText() { - return mTitleText; - } - - public void setDescription(String description) { - mDescription = description; - } - - public String getDescription() { - return mDescription; - } - - public long getStartTime() { - return mStartTime; - } - - public int getLengthInSecond() { - return mLengthInSecond; - } - - public long getStartTimeUtcMillis() { - return ConvertUtils.convertGPSTimeToUnixEpoch(mStartTime) * DateUtils.SECOND_IN_MILLIS; - } - - public long getEndTimeUtcMillis() { - return ConvertUtils.convertGPSTimeToUnixEpoch( - mStartTime + mLengthInSecond) * DateUtils.SECOND_IN_MILLIS; - } - - public String getContentRating() { - return mContentRating; - } - - @Override - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - @Override - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - - public String getBroadcastGenre() { - return mBroadcastGenre; - } - - public String getCanonicalGenre() { - return mCanonicalGenre; - } - - @Override - public void setHasCaptionTrack() { - mHasCaptionTrack = true; - } - - @Override - public boolean hasCaptionTrack() { - return mHasCaptionTrack; - } - - @Override - public int compareTo(@NonNull EitItem item) { - // The list of caption tracks and the program ids are not compared in here because the - // channels in TIF have the concept of the caption and audio tracks while the programs - // do not and the programs in TIF only have a program id since they are the rows of - // Content Provider. - int ret = mEventId - item.getEventId(); - if (ret != 0) { - return ret; - } - ret = StringUtils.compare(mTitleText, item.getTitleText()); - if (ret != 0) { - return ret; - } - if (mStartTime > item.getStartTime()) { - return 1; - } else if (mStartTime < item.getStartTime()) { - return -1; - } - if (mLengthInSecond > item.getLengthInSecond()) { - return 1; - } else if (mLengthInSecond < item.getLengthInSecond()) { - return -1; - } - - // Compares content ratings - ret = StringUtils.compare(mContentRating, item.getContentRating()); - if (ret != 0) { - return ret; - } - - // Compares broadcast genres - ret = StringUtils.compare(mBroadcastGenre, item.getBroadcastGenre()); - if (ret != 0) { - return ret; - } - // Compares canonical genres - ret = StringUtils.compare(mCanonicalGenre, item.getCanonicalGenre()); - if (ret != 0) { - return ret; - } - - // Compares descriptions - return StringUtils.compare(mDescription, item.getDescription()); - } - - public String getAudioLanguage() { - if (mAudioTracks == null) { - return ""; - } - ArrayList<String> languages = new ArrayList<>(); - for (AtscAudioTrack audioTrack : mAudioTracks) { - languages.add(audioTrack.language); - } - return TextUtils.join(",", languages); - } - - @Override - public String toString() { - return String.format(Locale.US, - "EitItem programId: %d, eventId: %d, title: %s, startTime: %10d, " - + "length: %6d, rating: %s, audio tracks: %d, caption tracks: %d, " - + "genres (broadcast: %s, canonical: %s), description: %s", - mProgramId, mEventId, mTitleText, mStartTime, mLengthInSecond, mContentRating, - mAudioTracks != null ? mAudioTracks.size() : 0, - mCaptionTracks != null ? mCaptionTracks.size() : 0, - mBroadcastGenre, mCanonicalGenre, mDescription); - } - } - - public static class EttItem { - public final int eventId; - public final String text; - - public EttItem(int eventId, String text) { - this.eventId = eventId; - this.text = text; - } - } -} diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java deleted file mode 100644 index 1cf514c1..00000000 --- a/src/com/android/tv/tuner/data/TunerChannel.java +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.data; - -import android.support.annotation.NonNull; -import android.util.Log; - -import com.android.tv.tuner.data.nano.Channel; -import com.android.tv.tuner.data.nano.Channel.TunerChannelProto; -import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.util.Ints; -import com.android.tv.util.StringUtils; -import com.google.protobuf.nano.MessageNano; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * A class that represents a single channel accessible through a tuner. - */ -public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracksInterface { - private static final String TAG = "TunerChannel"; - - /** - * Channel number separator between major number and minor number. - */ - public static final char CHANNEL_NUMBER_SEPARATOR = '-'; - - // See ATSC Code Points Registry. - private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] { - "ATSC Reserved", - "Analog television channels", - "ATSC_digital_television", - "ATSC_audio", - "ATSC_data_only_service", - "Software Download", - "Unassociated/Small Screen Service", - "Parameterized Service", - "ATSC NRT Service", - "Extended Parameterized Service" }; - private static final String ATSC_SERVICE_TYPE_NAME_RESERVED = - ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED]; - - public static final int INVALID_FREQUENCY = -1; - - // According to RFC4259, The number of available PIDs ranges from 0 to 8191. - public static final int INVALID_PID = -1; - - // According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff. - public static final int INVALID_STREAMTYPE = -1; - - // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766 - private final TunerChannelProto mProto; - - private TunerChannel(PsipData.VctItem channel, int programNumber, - List<PsiData.PmtItem> pmtItems, int type) { - mProto = new TunerChannelProto(); - if (channel == null) { - mProto.shortName = ""; - mProto.tsid = 0; - mProto.programNumber = programNumber; - mProto.virtualMajor = 0; - mProto.virtualMinor = 0; - } else { - mProto.shortName = channel.getShortName(); - if (channel.getLongName() != null) { - mProto.longName = channel.getLongName(); - } - mProto.tsid = channel.getChannelTsid(); - mProto.programNumber = channel.getProgramNumber(); - mProto.virtualMajor = channel.getMajorChannelNumber(); - mProto.virtualMinor = channel.getMinorChannelNumber(); - if (channel.getDescription() != null) { - mProto.description = channel.getDescription(); - } - mProto.serviceType = channel.getServiceType(); - } - initProto(pmtItems, type); - } - - private void initProto(List<PsiData.PmtItem> pmtItems, int type) { - mProto.type = type; - mProto.channelId = -1L; - mProto.frequency = INVALID_FREQUENCY; - mProto.videoPid = INVALID_PID; - mProto.videoStreamType = INVALID_STREAMTYPE; - List<Integer> audioPids = new ArrayList<>(); - List<Integer> audioStreamTypes = new ArrayList<>(); - for (PsiData.PmtItem pmt : pmtItems) { - switch (pmt.getStreamType()) { - // MPEG ES stream video types - case Channel.MPEG1: - case Channel.MPEG2: - case Channel.H263: - case Channel.H264: - case Channel.H265: - mProto.videoPid = pmt.getEsPid(); - mProto.videoStreamType = pmt.getStreamType(); - break; - - // MPEG ES stream audio types - case Channel.MPEG1AUDIO: - case Channel.MPEG2AUDIO: - case Channel.MPEG2AACAUDIO: - case Channel.MPEG4LATMAACAUDIO: - case Channel.A52AC3AUDIO: - case Channel.EAC3AUDIO: - audioPids.add(pmt.getEsPid()); - audioStreamTypes.add(pmt.getStreamType()); - break; - - // Non MPEG ES stream types - case 0x100: // PmtItem.ES_PID_PCR: - mProto.pcrPid = pmt.getEsPid(); - break; - } - } - mProto.audioPids = Ints.toArray(audioPids); - mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); - mProto.audioTrackIndex = (audioPids.size() > 0) ? 0 : -1; - } - - private TunerChannel(int programNumber, int type, PsipData.SdtItem channel, - List<PsiData.PmtItem> pmtItems) { - mProto = new TunerChannelProto(); - mProto.tsid = 0; - mProto.virtualMajor = 0; - mProto.virtualMinor = 0; - if (channel == null) { - mProto.shortName = ""; - mProto.programNumber = programNumber; - } else { - mProto.shortName = channel.getServiceName(); - mProto.programNumber = channel.getServiceId(); - mProto.serviceType = channel.getServiceType(); - } - initProto(pmtItems, type); - } - - /** - * Initialize tuner channel with VCT items and PMT items. - */ - public TunerChannel(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - this(channel, 0, pmtItems, Channel.TYPE_TUNER); - } - - /** - * Initialize tuner channel with program number and PMT items. - */ - public TunerChannel(int programNumber, List<PsiData.PmtItem> pmtItems) { - this(null, programNumber, pmtItems, Channel.TYPE_TUNER); - } - - /** - * Initialize tuner channel with SDT items and PMT items. - */ - public TunerChannel(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - this(0, Channel.TYPE_TUNER, channel, pmtItems); - } - - private TunerChannel(TunerChannelProto tunerChannelProto) { - mProto = tunerChannelProto; - } - - public static TunerChannel forFile(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE); - } - - public static TunerChannel forDvbFile( - PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - return new TunerChannel(0, Channel.TYPE_FILE, channel, pmtItems); - } - - /** - * Create a TunerChannel object suitable for network tuners - * @param major Channel number major - * @param minor Channel number minor - * @param programNumber Program number - * @param shortName Short name - * @param recordingProhibited Recording prohibition info - * @param videoFormat Video format. Should be {@code null} or one of the followings: - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P}, - * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P} - * @return a TunerChannel object - */ - public static TunerChannel forNetwork(int major, int minor, int programNumber, - String shortName, boolean recordingProhibited, String videoFormat) { - TunerChannel tunerChannel = new TunerChannel( - null, programNumber, Collections.EMPTY_LIST, Channel.TYPE_NETWORK); - tunerChannel.setVirtualMajor(major); - tunerChannel.setVirtualMinor(minor); - tunerChannel.setShortName(shortName); - // Set audio and video pids in order to work around the audio-only channel check. - tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0))); - tunerChannel.selectAudioTrack(0); - tunerChannel.setVideoPid(0); - tunerChannel.setRecordingProhibited(recordingProhibited); - if (videoFormat != null) { - tunerChannel.setVideoFormat(videoFormat); - } - return tunerChannel; - } - - public String getName() { - return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName; - } - - public String getShortName() { - return mProto.shortName; - } - - public int getProgramNumber() { - return mProto.programNumber; - } - - public int getServiceType() { - return mProto.serviceType; - } - - public String getServiceTypeName() { - int serviceType = mProto.serviceType; - if (serviceType >= 0 && serviceType < ATSC_SERVICE_TYPE_NAMES.length) { - return ATSC_SERVICE_TYPE_NAMES[serviceType]; - } - return ATSC_SERVICE_TYPE_NAME_RESERVED; - } - - public int getVirtualMajor() { - return mProto.virtualMajor; - } - - public int getVirtualMinor() { - return mProto.virtualMinor; - } - - public int getFrequency() { - return mProto.frequency; - } - - public String getModulation() { - return mProto.modulation; - } - - public int getTsid() { - return mProto.tsid; - } - - public int getVideoPid() { - return mProto.videoPid; - } - - synchronized public void setVideoPid(int videoPid) { - mProto.videoPid = videoPid; - } - - public int getVideoStreamType() { - return mProto.videoStreamType; - } - - public int getAudioPid() { - if (mProto.audioTrackIndex == -1) { - return INVALID_PID; - } - return mProto.audioPids[mProto.audioTrackIndex]; - } - - public int getAudioStreamType() { - if (mProto.audioTrackIndex == -1) { - return INVALID_STREAMTYPE; - } - return mProto.audioStreamTypes[mProto.audioTrackIndex]; - } - - public List<Integer> getAudioPids() { - return Ints.asList(mProto.audioPids); - } - - synchronized public void setAudioPids(List<Integer> audioPids) { - mProto.audioPids = Ints.toArray(audioPids); - } - - public List<Integer> getAudioStreamTypes() { - return Ints.asList(mProto.audioStreamTypes); - } - - synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) { - mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); - } - - public int getPcrPid() { - return mProto.pcrPid; - } - - public int getType() { - return mProto.type; - } - - synchronized public void setFilepath(String filepath) { - mProto.filepath = filepath == null ? "" : filepath; - } - - public String getFilepath() { - return mProto.filepath; - } - - synchronized public void setVirtualMajor(int virtualMajor) { - mProto.virtualMajor = virtualMajor; - } - - synchronized public void setVirtualMinor(int virtualMinor) { - mProto.virtualMinor = virtualMinor; - } - - synchronized public void setShortName(String shortName) { - mProto.shortName = shortName == null ? "" : shortName; - } - - synchronized public void setFrequency(int frequency) { - mProto.frequency = frequency; - } - - synchronized public void setModulation(String modulation) { - mProto.modulation = modulation == null ? "" : modulation; - } - - public boolean hasVideo() { - return mProto.videoPid != INVALID_PID; - } - - public boolean hasAudio() { - return getAudioPid() != INVALID_PID; - } - - public long getChannelId() { - return mProto.channelId; - } - - synchronized public void setChannelId(long channelId) { - mProto.channelId = channelId; - } - - public String getDisplayNumber() { - return getDisplayNumber(true); - } - - public String getDisplayNumber(boolean ignoreZeroMinorNumber) { - if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) { - return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR, - mProto.virtualMinor); - } else if (mProto.virtualMajor != 0) { - return Integer.toString(mProto.virtualMajor); - } else { - return Integer.toString(mProto.programNumber); - } - } - - public String getDescription() { - return mProto.description; - } - - @Override - synchronized public void setHasCaptionTrack() { - mProto.hasCaptionTrack = true; - } - - @Override - public boolean hasCaptionTrack() { - return mProto.hasCaptionTrack; - } - - @Override - public List<AtscAudioTrack> getAudioTracks() { - return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); - } - - synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) { - mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); - } - - @Override - public List<AtscCaptionTrack> getCaptionTracks() { - return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); - } - - synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) { - mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]); - } - - synchronized public void selectAudioTrack(int index) { - if (0 <= index && index < mProto.audioPids.length) { - mProto.audioTrackIndex = index; - } else { - mProto.audioTrackIndex = -1; - } - } - - synchronized public void setRecordingProhibited(boolean recordingProhibited) { - mProto.recordingProhibited = recordingProhibited; - } - - public boolean isRecordingProhibited() { - return mProto.recordingProhibited; - } - - synchronized public void setVideoFormat(String videoFormat) { - mProto.videoFormat = videoFormat == null ? "" : videoFormat; - } - - public String getVideoFormat() { - return mProto.videoFormat; - } - - @Override - public String toString() { - switch (mProto.type) { - case Channel.TYPE_FILE: - return String.format("{%d-%d %s} Filepath: %s, ProgramNumber %d", - mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, - mProto.filepath, mProto.programNumber); - //case Channel.TYPE_TUNER: - default: - return String.format("{%d-%d %s} Frequency: %d, ProgramNumber %d", - mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, - mProto.frequency, mProto.programNumber); - } - } - - @Override - public int compareTo(@NonNull TunerChannel channel) { - // In the same frequency, the program number acts as the sub-channel number. - int ret = getFrequency() - channel.getFrequency(); - if (ret != 0) { - return ret; - } - ret = getProgramNumber() - channel.getProgramNumber(); - if (ret != 0) { - return ret; - } - ret = StringUtils.compare(getName(), channel.getName()); - if (ret != 0) { - return ret; - } - // For FileTsStreamer, file paths should be compared. - return StringUtils.compare(getFilepath(), channel.getFilepath()); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TunerChannel)) { - return false; - } - return compareTo((TunerChannel) o) == 0; - } - - @Override - public int hashCode() { - return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath()); - } - - // Serialization - synchronized public byte[] toByteArray() { - try { - return MessageNano.toByteArray(mProto); - } catch (Exception e) { - // Retry toByteArray. b/34197766 - Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock", - e); - return MessageNano.toByteArray(mProto); - } - } - - public static TunerChannel parseFrom(byte[] data) { - if (data == null) { - return null; - } - try { - return new TunerChannel(TunerChannelProto.parseFrom(data)); - } catch (IOException e) { - Log.e(TAG, "Could not parse from byte array", e); - return null; - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java deleted file mode 100644 index 5f536708..00000000 --- a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.util.Log; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaClock; -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.TrackRenderer; -import com.google.android.exoplayer.util.Assertions; -import com.android.tv.tuner.cc.Cea708Parser; -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; - -import java.io.IOException; - -/** - * A {@link TrackRenderer} for CEA-708 textual subtitles. - */ -public class Cea708TextTrackRenderer extends TrackRenderer implements - Cea708Parser.OnCea708ParserListener { - private static final String TAG = "Cea708TextTrackRenderer"; - private static final boolean DEBUG = false; - - public static final int MSG_SERVICE_NUMBER = 1; - public static final int MSG_ENABLE_CLOSED_CAPTION = 2; - - // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. - private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8; - - private final SampleSource.SampleSourceReader mSource; - private final SampleHolder mSampleHolder; - private final MediaFormatHolder mFormatHolder; - private int mServiceNumber; - private boolean mInputStreamEnded; - private long mCurrentPositionUs; - private long mPresentationTimeUs; - private int mTrackIndex; - private boolean mRenderingDisabled; - private Cea708Parser mCea708Parser; - private CcListener mCcListener; - - public interface CcListener { - void emitEvent(CaptionEvent captionEvent); - void clearCaption(); - void discoverServiceNumber(int serviceNumber); - } - - public Cea708TextTrackRenderer(SampleSource source) { - mSource = source.register(); - mTrackIndex = -1; - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mFormatHolder = new MediaFormatHolder(); - } - - @Override - protected MediaClock getMediaClock() { - return null; - } - - private boolean handlesMimeType(String mimeType) { - return mimeType.equals(MpegTsSampleExtractor.MIMETYPE_TEXT_CEA_708); - } - - @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return false; - } - int trackCount = mSource.getTrackCount(); - for (int i = 0; i < trackCount; ++i) { - MediaFormat trackFormat = mSource.getFormat(i); - if (handlesMimeType(trackFormat.mimeType)) { - mTrackIndex = i; - clearDecodeState(); - return true; - } - } - // TODO: Check this case. (Source do not have the proper mime type.) - return true; - } - - @Override - protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); - mSource.enable(mTrackIndex, positionUs); - mInputStreamEnded = false; - mPresentationTimeUs = positionUs; - mCurrentPositionUs = Long.MIN_VALUE; - } - - @Override - protected void onDisabled() { - mSource.disable(mTrackIndex); - } - - @Override - protected void onReleased() { - mSource.release(); - mCea708Parser = null; - } - - @Override - protected boolean isEnded() { - return mInputStreamEnded; - } - - @Override - protected boolean isReady() { - // Since this track will be fed by {@link VideoTrackRenderer}, - // it is not required to control transition between ready state and buffering state. - return true; - } - - @Override - protected int getTrackCount() { - return mTrackIndex < 0 ? 0 : 1; - } - - @Override - protected MediaFormat getFormat(int track) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); - return mSource.getFormat(mTrackIndex); - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - try { - mSource.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - try { - mPresentationTimeUs = positionUs; - if (!mInputStreamEnded) { - processOutput(); - feedInputBuffer(); - } - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - private boolean processOutput() { - return !mInputStreamEnded && mCea708Parser != null && - mCea708Parser.processClosedCaptions(mPresentationTimeUs); - } - - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { - if (mInputStreamEnded) { - return false; - } - long discontinuity = mSource.readDiscontinuity(mTrackIndex); - if (discontinuity != SampleSource.NO_DISCONTINUITY) { - if (DEBUG) { - Log.d(TAG, "Read discontinuity happened"); - } - - // TODO: handle input discontinuity for trickplay. - clearDecodeState(); - mPresentationTimeUs = discontinuity; - return false; - } - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - int result = mSource.readData(mTrackIndex, mPresentationTimeUs, - mFormatHolder, mSampleHolder); - switch (result) { - case SampleSource.NOTHING_READ: { - return false; - } - case SampleSource.FORMAT_READ: { - if (DEBUG) { - Log.i(TAG, "Format was read again"); - } - return true; - } - case SampleSource.END_OF_STREAM: { - if (DEBUG) { - Log.i(TAG, "End of stream from SampleSource"); - } - mInputStreamEnded = true; - return false; - } - case SampleSource.SAMPLE_READ: { - mSampleHolder.data.flip(); - if (mCea708Parser != null && !mRenderingDisabled) { - mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs); - } - return true; - } - } - return false; - } - - private void clearDecodeState() { - mCea708Parser = new Cea708Parser(); - mCea708Parser.setListener(this); - mCea708Parser.setListenServiceNumber(mServiceNumber); - } - - @Override - protected long getDurationUs() { - return mSource.getFormat(mTrackIndex).durationUs; - } - - @Override - protected long getBufferedPositionUs() { - return mSource.getBufferedPositionUs(); - } - - @Override - protected void seekTo(long currentPositionUs) throws ExoPlaybackException { - mSource.seekToUs(currentPositionUs); - mInputStreamEnded = false; - mPresentationTimeUs = currentPositionUs; - mCurrentPositionUs = Long.MIN_VALUE; - } - - @Override - protected void onStarted() { - // do nothing. - } - - @Override - protected void onStopped() { - // do nothing. - } - - private void setServiceNumber(int serviceNumber) { - mServiceNumber = serviceNumber; - if (mCea708Parser != null) { - mCea708Parser.setListenServiceNumber(serviceNumber); - } - } - - @Override - public void emitEvent(CaptionEvent event) { - if (mCcListener != null) { - mCcListener.emitEvent(event); - } - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - if (mCcListener != null) { - mCcListener.discoverServiceNumber(serviceNumber); - } - } - - public void setCcListener(CcListener ccListener) { - mCcListener = ccListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_SERVICE_NUMBER: - setServiceNumber((int) message); - break; - case MSG_ENABLE_CLOSED_CAPTION: - boolean renderingDisabled = (Boolean) message == false; - if (mRenderingDisabled != renderingDisabled) { - mRenderingDisabled = renderingDisabled; - if (mRenderingDisabled) { - if (mCea708Parser != null) { - mCea708Parser.clear(); - } - if (mCcListener != null) { - mCcListener.clearCaption(); - } - } - } - break; - default: - super.handleMessage(messageType, message); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java deleted file mode 100644 index 0ab6d8c4..00000000 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java +++ /dev/null @@ -1,41 +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.exoplayer; - -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; -import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; - -import java.util.ArrayList; -import java.util.List; - -/** - * Extractor factory, mainly aim at create TsExtractor with FLAG_ALLOW_NON_IDR_KEYFRAMES flags for - * H.264 stream - */ -public final class ExoPlayerExtractorsFactory implements ExtractorsFactory { - @Override - public Extractor[] createExtractors() { - // Only create TsExtractor since we only target MPEG2TS stream. - Extractor[] extractors = { - new TsExtractor(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory( - DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES), false) }; - return extractors; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java deleted file mode 100644 index 0b648400..00000000 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ /dev/null @@ -1,552 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.net.Uri; -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.util.Pair; - -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -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.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A class that extracts samples from a live broadcast stream while storing the sample on the disk. - * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. - */ -public class ExoPlayerSampleExtractor implements SampleExtractor { - private static final String TAG = "ExoPlayerSampleExtracto"; - - private static final int INVALID_TRACK_INDEX = -1; - private final HandlerThread mSourceReaderThread; - private final long mId; - - private final Handler.Callback mSourceReaderWorker; - - private BufferManager.SampleBuffer mSampleBuffer; - private Handler mSourceReaderHandler; - private volatile boolean mPrepared; - private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); - private IOException mExceptionOnPrepare; - private List<MediaFormat> mTrackFormats; - private int mVideoTrackIndex = INVALID_TRACK_INDEX; - private boolean mVideoTrackMet; - private long mBaseSamplePts = Long.MIN_VALUE; - private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); - private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>(); - private OnCompletionListener mOnCompletionListener; - private Handler mOnCompletionListenerHandler; - private IOException mError; - - public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager, - PlaybackBufferListener bufferListener, boolean isRecording) { - // It'll be used as a timeshift file chunk name's prefix. - mId = System.currentTimeMillis(); - - EventListener eventListener = new EventListener() { - @Override - public void onLoadError(IOException error) { - mError = error; - } - }; - - mSourceReaderThread = new HandlerThread("SourceReaderThread"); - mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri, - new com.google.android.exoplayer2.upstream.DataSource.Factory() { - @Override - public com.google.android.exoplayer2.upstream.DataSource createDataSource() { - // Returns an adapter implementation for ExoPlayer V2 DataSource interface. - return new com.google.android.exoplayer2.upstream.DataSource() { - @Override - public long open(DataSpec dataSpec) throws IOException { - return source.open( - new com.google.android.exoplayer.upstream.DataSpec( - dataSpec.uri, dataSpec.postBody, - dataSpec.absoluteStreamPosition, dataSpec.position, - dataSpec.length, dataSpec.key, dataSpec.flags)); - } - - @Override - public int read(byte[] buffer, int offset, int readLength) - throws IOException { - return source.read(buffer, offset, readLength); - } - - @Override - public Uri getUri() { - return null; - } - - @Override - public void close() throws IOException { - source.close(); - } - }; - } - }, - new ExoPlayerExtractorsFactory(), - // Do not create a handler if we not on a looper. e.g. test. - Looper.myLooper() != null ? new Handler() : null, eventListener)); - if (isRecording) { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, - RecordingSampleBuffer.BUFFER_REASON_RECORDING); - } else { - if (bufferManager == null) { - mSampleBuffer = new SimpleSampleBuffer(bufferListener); - } else { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, - RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); - } - } - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { - mOnCompletionListener = listener; - mOnCompletionListenerHandler = handler; - } - - private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback { - public static final int MSG_PREPARE = 1; - public static final int MSG_FETCH_SAMPLES = 2; - public static final int MSG_RELEASE = 3; - private static final int RETRY_INTERVAL_MS = 50; - - private final MediaSource mSampleSource; - private MediaPeriod mMediaPeriod; - private SampleStream[] mStreams; - private boolean[] mTrackMetEos; - private boolean mMetEos = false; - private long mCurrentPosition; - private DecoderInputBuffer mDecoderInputBuffer; - private SampleHolder mSampleHolder; - private boolean mPrepareRequested; - - public SourceReaderWorker(MediaSource sampleSource) { - mSampleSource = sampleSource; - mSampleSource.prepareSource(null, false, new MediaSource.Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - // Dynamic stream change is not supported yet. b/28169263 - // For now, this will cause EOS and playback reset. - } - }); - mDecoderInputBuffer = new DecoderInputBuffer( - DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - } - - MediaFormat convertFormat(Format format) { - if (format.sampleMimeType.startsWith("audio/")) { - return MediaFormat.createAudioFormat(format.id, format.sampleMimeType, - format.bitrate, format.maxInputSize, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount, - format.sampleRate, format.initializationData, format.language, - format.pcmEncoding); - } else if (format.sampleMimeType.startsWith("video/")) { - return MediaFormat.createVideoFormat( - format.id, format.sampleMimeType, format.bitrate, format.maxInputSize, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height, - format.initializationData, format.rotationDegrees, - format.pixelWidthHeightRatio, format.projectionData, format.stereoMode); - } else if (format.sampleMimeType.endsWith("/cea-608") - || format.sampleMimeType.startsWith("text/")) { - return MediaFormat.createTextFormat( - format.id, format.sampleMimeType, format.bitrate, - com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language); - } else { - return MediaFormat.createFormatForMimeType( - format.id, format.sampleMimeType, format.bitrate, - com.google.android.exoplayer.C.UNKNOWN_TIME_US); - } - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - if (mMediaPeriod == null) { - // 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); - } - boolean retain[] = new boolean[trackGroupArray.length]; - boolean reset[] = new boolean[trackGroupArray.length]; - mStreams = new SampleStream[trackGroupArray.length]; - mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0); - if (mTrackFormats == null) { - int trackCount = trackGroupArray.length; - mTrackMetEos = new boolean[trackCount]; - List<MediaFormat> trackFormats = new ArrayList<>(); - int videoTrackCount = 0; - for (int i = 0; i < trackCount; i++) { - Format format = trackGroupArray.get(i).getFormat(0); - if (format.sampleMimeType.startsWith("video/")) { - videoTrackCount++; - mVideoTrackIndex = i; - } - trackFormats.add(convertFormat(format)); - } - if (videoTrackCount > 1) { - // Disable dropping samples when there are multiple video tracks. - mVideoTrackIndex = INVALID_TRACK_INDEX; - } - mTrackFormats = trackFormats; - List<String> ids = new ArrayList<>(); - for (int i = 0; i < mTrackFormats.size(); i++) { - ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); - } - try { - mSampleBuffer.init(ids, mTrackFormats); - } catch (IOException e) { - // In this case, we will not schedule any further operation. - // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will - // call release() eventually. - mExceptionOnPrepare = e; - return; - } - mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); - mPrepared = true; - } - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - source.continueLoading(mCurrentPosition); - } - - @Override - public boolean handleMessage(Message message) { - switch (message.what) { - case MSG_PREPARE: - if (!mPrepareRequested) { - mPrepareRequested = true; - mMediaPeriod = mSampleSource.createPeriod(0, - new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0); - mMediaPeriod.prepare(this); - try { - mMediaPeriod.maybeThrowPrepareError(); - } catch (IOException e) { - mError = e; - } - } - return true; - case MSG_FETCH_SAMPLES: - boolean didSomething = false; - ConditionVariable conditionVariable = new ConditionVariable(); - int trackCount = mStreams.length; - for (int i = 0; i < trackCount; ++i) { - if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ - != fetchSample(i, mSampleHolder, conditionVariable)) { - if (mMetEos) { - // If mMetEos was on during fetchSample() due to an error, - // fetching from other tracks is not necessary. - break; - } - didSomething = true; - } - } - mMediaPeriod.continueLoading(mCurrentPosition); - if (!mMetEos) { - if (didSomething) { - mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); - } else { - mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES, - RETRY_INTERVAL_MS); - } - } else { - notifyCompletionIfNeeded(false); - } - return true; - case MSG_RELEASE: - if (mMediaPeriod != null) { - mSampleSource.releasePeriod(mMediaPeriod); - mSampleSource.releaseSource(); - mMediaPeriod = null; - } - cleanUp(); - mSourceReaderHandler.removeCallbacksAndMessages(null); - return true; - } - return false; - } - - private int fetchSample(int track, SampleHolder sample, - ConditionVariable conditionVariable) { - FormatHolder dummyFormatHolder = new FormatHolder(); - mDecoderInputBuffer.clear(); - int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer); - if (ret == C.RESULT_BUFFER_READ - // Double-check if the extractor provided the data to prevent NPE. b/33758354 - && mDecoderInputBuffer.data != null) { - if (mCurrentPosition < mDecoderInputBuffer.timeUs) { - mCurrentPosition = mDecoderInputBuffer.timeUs; - } - try { - Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); - if (lastExtractedPositionUs == null) { - mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs); - } else { - mLastExtractedPositionUsMap.put(track, - Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); - } - queueSample(track, conditionVariable); - } catch (IOException e) { - mLastExtractedPositionUsMap.clear(); - mMetEos = true; - mSampleBuffer.setEos(); - } - } else if (ret == C.RESULT_END_OF_INPUT) { - mTrackMetEos[track] = true; - for (int i = 0; i < mTrackMetEos.length; ++i) { - if (!mTrackMetEos[i]) { - break; - } - if (i == mTrackMetEos.length - 1) { - mMetEos = true; - mSampleBuffer.setEos(); - } - } - } - // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 - return ret; - } - - private void queueSample(int index, ConditionVariable conditionVariable) - throws IOException { - if (mVideoTrackIndex != INVALID_TRACK_INDEX) { - if (!mVideoTrackMet) { - if (index != mVideoTrackIndex) { - SampleHolder sample = - new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - mSampleHolder.flags = - (mDecoderInputBuffer.isKeyFrame() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) - | (mDecoderInputBuffer.isDecodeOnly() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY - : 0); - sample.timeUs = mDecoderInputBuffer.timeUs; - sample.size = mDecoderInputBuffer.data.position(); - sample.ensureSpaceForWrite(sample.size); - mDecoderInputBuffer.flip(); - sample.data.position(0); - sample.data.put(mDecoderInputBuffer.data); - sample.data.flip(); - mPendingSamples.add(new Pair<>(index, sample)); - return; - } - mVideoTrackMet = true; - mBaseSamplePts = - mDecoderInputBuffer.timeUs - - MpegTsDefaultAudioTrackRenderer - .INITIAL_AUDIO_BUFFERING_TIME_US; - for (Pair<Integer, SampleHolder> pair : mPendingSamples) { - if (pair.second.timeUs >= mBaseSamplePts) { - mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable); - } - } - mPendingSamples.clear(); - } else { - if (mDecoderInputBuffer.timeUs < mBaseSamplePts - && mVideoTrackIndex != index) { - return; - } - } - } - // Copy the decoder input to the sample holder. - mSampleHolder.clearData(); - mSampleHolder.flags = - (mDecoderInputBuffer.isKeyFrame() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) - | (mDecoderInputBuffer.isDecodeOnly() - ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0); - mSampleHolder.timeUs = mDecoderInputBuffer.timeUs; - mSampleHolder.size = mDecoderInputBuffer.data.position(); - mSampleHolder.ensureSpaceForWrite(mSampleHolder.size); - mDecoderInputBuffer.flip(); - mSampleHolder.data.position(0); - mSampleHolder.data.put(mDecoderInputBuffer.data); - mSampleHolder.data.flip(); - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable); - - // Checks whether the storage has enough bandwidth for recording samples. - if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { - mSampleBuffer.handleWriteSpeedSlow(); - } - } - } - - @Override - public void maybeThrowError() throws IOException { - if (mError != null) { - IOException e = mError; - mError = null; - throw e; - } - } - - @Override - public boolean prepare() throws IOException { - if (!mSourceReaderThread.isAlive()) { - mSourceReaderThread.start(); - mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(), - mSourceReaderWorker); - mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); - } - if (mExceptionOnPrepare != null) { - throw mExceptionOnPrepare; - } - return mPrepared; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - outMediaFormatHolder.format = mTrackFormats.get(track); - outMediaFormatHolder.drmInitData = null; - } - - @Override - public void selectTrack(int index) { - mSampleBuffer.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - mSampleBuffer.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleBuffer.getBufferedPositionUs(); - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleBuffer.continueBuffering(positionUs); - } - - @Override - public void seekTo(long positionUs) { - mSampleBuffer.seekTo(positionUs); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return mSampleBuffer.readSample(track, sampleHolder); - } - - @Override - public void release() { - if (mSourceReaderThread.isAlive()) { - mSourceReaderHandler.removeCallbacksAndMessages(null); - mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); - mSourceReaderThread.quitSafely(); - // Return early in this case so that session worker can start working on the next - // request as early as it can. The clean up will be done in the reader thread while - // handling MSG_RELEASE. - } else { - cleanUp(); - } - } - - private void cleanUp() { - boolean result = true; - try { - if (mSampleBuffer != null) { - mSampleBuffer.release(); - mSampleBuffer = null; - } - } catch (IOException e) { - result = false; - } - notifyCompletionIfNeeded(result); - setOnCompletionListener(null, null); - } - - private void notifyCompletionIfNeeded(final boolean result) { - if (!mOnCompletionCalled.getAndSet(true)) { - final OnCompletionListener listener = mOnCompletionListener; - final long lastExtractedPositionUs = getLastExtractedPositionUs(); - if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { - mOnCompletionListenerHandler.post(new Runnable() { - @Override - public void run() { - listener.onCompletion(result, lastExtractedPositionUs); - } - }); - } - } - } - - private long getLastExtractedPositionUs() { - long lastExtractedPositionUs = Long.MIN_VALUE; - for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) { - if (mVideoTrackIndex != entry.getKey()) { - lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue()); - } - } - if (lastExtractedPositionUs == Long.MIN_VALUE) { - lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US; - } - return lastExtractedPositionUs; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java deleted file mode 100644 index b7e42a7c..00000000 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -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.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import android.os.Handler; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * A class that plays a recorded stream without using {@link android.media.MediaExtractor}, - * since all samples are extracted and stored to the permanent storage already. - */ -public class FileSampleExtractor implements SampleExtractor{ - private static final String TAG = "FileSampleExtractor"; - private static final boolean DEBUG = false; - - private int mTrackCount; - private boolean mReleased; - - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - private BufferManager.SampleBuffer mSampleBuffer; - - public FileSampleExtractor( - BufferManager bufferManager, PlaybackBufferListener bufferListener) { - mBufferManager = bufferManager; - mBufferListener = bufferListener; - mTrackCount = -1; - } - - @Override - public void maybeThrowError() throws IOException { - // Do nothing. - } - - @Override - public boolean prepare() throws IOException { - List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles(); - if (trackFormatList == null || trackFormatList.isEmpty()) { - throw new IOException("Cannot find meta files for the recording."); - } - mTrackCount = trackFormatList.size(); - List<String> ids = new ArrayList<>(); - mTrackFormats.clear(); - for (int i = 0; i < mTrackCount; ++i) { - BufferManager.TrackFormat trackFormat = trackFormatList.get(i); - ids.add(trackFormat.trackId); - mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format)); - } - mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true, - RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK); - mSampleBuffer.init(ids, mTrackFormats); - return true; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - outMediaFormatHolder.format = mTrackFormats.get(track); - outMediaFormatHolder.drmInitData = null; - } - - @Override - public void release() { - if (!mReleased) { - if (mSampleBuffer != null) { - try { - mSampleBuffer.release(); - } catch (IOException e) { - // Do nothing. Playback ends now. - } - } - } - mReleased = true; - } - - @Override - public void selectTrack(int index) { - mSampleBuffer.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - mSampleBuffer.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleBuffer.getBufferedPositionUs(); - } - - @Override - public void seekTo(long positionUs) { - mSampleBuffer.seekTo(positionUs); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return mSampleBuffer.readSample(track, sampleHolder); - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleBuffer.continueBuffering(positionUs); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java deleted file mode 100644 index 2694298a..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ /dev/null @@ -1,696 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.content.Context; -import android.media.AudioFormat; -import android.media.MediaCodec.CryptoException; -import android.media.PlaybackParams; -import android.os.Handler; -import android.support.annotation.IntDef; -import android.view.Surface; - -import com.google.android.exoplayer.DummyTrackRenderer; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.upstream.DataSource; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.data.Cea708Data; -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.TunerChannel; -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 java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** MPEG-2 TS stream player implementation using ExoPlayer. */ -public class MpegTsPlayer - implements ExoPlayer.Listener, - MediaCodecVideoTrackRenderer.EventListener, - MpegTsDefaultAudioTrackRenderer.EventListener, - MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener { - private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; - - /** - * Interface definition for building specific track renderers. - */ - public interface RendererBuilder { - void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - boolean hasSoftwareAudioDecoder, RendererBuilderCallback callback); - } - - /** - * Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. - */ - public interface RendererBuilderCallback { - void onRenderers(String[][] trackNames, TrackRenderer[] renderers); - void onRenderersError(Exception e); - } - - /** - * Interface definition for a callback to be notified of changes in player state. - */ - public interface Listener { - void onStateChanged(boolean playWhenReady, int playbackState); - void onError(Exception e); - void onVideoSizeChanged(int width, int height, - float pixelWidthHeightRatio); - void onDrawnToSurface(MpegTsPlayer player, Surface surface); - void onAudioUnplayable(); - void onSmoothTrickplayForceStopped(); - } - - /** - * Interface definition for a callback to be notified of changes on video display. - */ - public interface VideoEventListener { - /** - * Notifies the caption event. - */ - void onEmitCaptionEvent(CaptionEvent event); - - /** - * Notifies clearing up whole closed caption event. - */ - void onClearCaptionEvent(); - - /** - * Notifies the discovered caption service number. - */ - void onDiscoverCaptionServiceNumber(int serviceNumber); - } - - public static final int RENDERER_COUNT = 3; - public static final int MIN_BUFFER_MS = 0; - public static final int MIN_REBUFFER_MS = 500; - - @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT}) - @Retention(RetentionPolicy.SOURCE) - public @interface TrackType {} - public static final int TRACK_TYPE_VIDEO = 0; - public static final int TRACK_TYPE_AUDIO = 1; - public static final int TRACK_TYPE_TEXT = 2; - - @IntDef({RENDERER_BUILDING_STATE_IDLE, RENDERER_BUILDING_STATE_BUILDING, - RENDERER_BUILDING_STATE_BUILT}) - @Retention(RetentionPolicy.SOURCE) - public @interface RendererBuildingState {} - private static final int RENDERER_BUILDING_STATE_IDLE = 1; - private static final int RENDERER_BUILDING_STATE_BUILDING = 2; - private static final int RENDERER_BUILDING_STATE_BUILT = 3; - - private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f; - private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f; - - private final RendererBuilder mRendererBuilder; - private final ExoPlayer mPlayer; - private final Handler mMainHandler; - private final AudioCapabilities mAudioCapabilities; - private final TsDataSourceManager mSourceManager; - - private Listener mListener; - @RendererBuildingState private int mRendererBuildingState; - - private Surface mSurface; - private TsDataSource mDataSource; - private InternalRendererBuilderCallback mBuilderCallback; - private TrackRenderer mVideoRenderer; - private TrackRenderer mAudioRenderer; - private Cea708TextTrackRenderer mTextRenderer; - private final Cea708TextTrackRenderer.CcListener mCcListener; - private VideoEventListener mVideoEventListener; - private boolean mTrickplayRunning; - private float mVolume; - - /** - * Creates MPEG2-TS stream player. - * - * @param rendererBuilder the builder of track renderers - * @param handler the handler for the playback events in track renderers - * @param sourceManager the manager for {@link DataSource} - * @param capabilities the {@link AudioCapabilities} of the current device - * @param listener the listener for playback state changes - */ - public MpegTsPlayer(RendererBuilder rendererBuilder, Handler handler, - TsDataSourceManager sourceManager, AudioCapabilities capabilities, - Listener listener) { - mRendererBuilder = rendererBuilder; - mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); - mPlayer.addListener(this); - mMainHandler = handler; - mAudioCapabilities = capabilities; - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - mCcListener = new MpegTsCcListener(); - mSourceManager = sourceManager; - mListener = listener; - } - - /** - * Sets the video event listener. - * - * @param videoEventListener the listener for video events - */ - public void setVideoEventListener(VideoEventListener videoEventListener) { - mVideoEventListener = videoEventListener; - } - - /** - * Sets the closed caption service number. - * - * @param captionServiceNumber the service number of CEA-708 closed caption - */ - public void setCaptionServiceNumber(int captionServiceNumber) { - mCaptionServiceNumber = captionServiceNumber; - if (mTextRenderer != null) { - mPlayer.sendMessage(mTextRenderer, - Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); - } - } - - /** - * Sets the surface for the player. - * - * @param surface the {@link Surface} to render video - */ - public void setSurface(Surface surface) { - mSurface = surface; - pushSurface(false); - } - - /** - * Returns the current surface of the player. - */ - public Surface getSurface() { - return mSurface; - } - - /** - * Clears the surface and waits until the surface is being cleaned. - */ - public void blockingClearSurface() { - mSurface = null; - pushSurface(true); - } - - /** - * Creates renderers and {@link DataSource} and initializes player. - * @param context a {@link Context} instance - * @param channel to play - * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder - * @param eventListener for program information which will be scanned from MPEG2-TS stream - * @return true when everything is created and initialized well, false otherwise - */ - public boolean prepare(Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder, - EventDetector.EventListener eventListener) { - TsDataSource source = null; - if (channel != null) { - source = mSourceManager.createDataSource(context, channel, eventListener); - if (source == null) { - return false; - } - } - mDataSource = source; - if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { - mPlayer.stop(); - } - if (mBuilderCallback != null) { - mBuilderCallback.cancel(); - } - mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; - mBuilderCallback = new InternalRendererBuilderCallback(); - mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); - return true; - } - - /** - * Returns {@link TsDataSource} which provides MPEG2-TS stream. - */ - public TsDataSource getDataSource() { - return mDataSource; - } - - private void onRenderers(TrackRenderer[] renderers) { - mBuilderCallback = null; - for (int i = 0; i < RENDERER_COUNT; i++) { - if (renderers[i] == null) { - // Convert a null renderer to a dummy renderer. - renderers[i] = new DummyTrackRenderer(); - } - } - mVideoRenderer = renderers[TRACK_TYPE_VIDEO]; - mAudioRenderer = renderers[TRACK_TYPE_AUDIO]; - mTextRenderer = (Cea708TextTrackRenderer) renderers[TRACK_TYPE_TEXT]; - mTextRenderer.setCcListener(mCcListener); - mPlayer.sendMessage( - mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); - mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT; - pushSurface(false); - mPlayer.prepare(renderers); - pushTrackSelection(TRACK_TYPE_VIDEO, true); - pushTrackSelection(TRACK_TYPE_AUDIO, true); - pushTrackSelection(TRACK_TYPE_TEXT, true); - } - - private void onRenderersError(Exception e) { - mBuilderCallback = null; - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - if (mListener != null) { - mListener.onError(e); - } - } - - /** - * Sets the player state to pause or play. - * - * @param playWhenReady sets the player state to being ready to play when {@code true}, - * sets the player state to being paused when {@code false} - * - */ - public void setPlayWhenReady(boolean playWhenReady) { - mPlayer.setPlayWhenReady(playWhenReady); - stopSmoothTrickplay(false); - } - - /** - * Returns true, if trickplay is supported. - */ - public boolean supportSmoothTrickPlay(float playbackSpeed) { - return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED - && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED; - } - - /** - * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called. - */ - public void startSmoothTrickplay(PlaybackParams playbackParams) { - SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); - mPlayer.setPlayWhenReady(true); - mTrickplayRunning = true; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, - MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, - playbackParams.getSpeed()); - } else { - mPlayer.sendMessage(mAudioRenderer, - MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, - playbackParams); - } - } - - private void stopSmoothTrickplay(boolean calledBySeek) { - if (mTrickplayRunning) { - mTrickplayRunning = false; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, - 1.0f); - } else { - mPlayer.sendMessage(mAudioRenderer, - MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, - new PlaybackParams().setSpeed(1.0f)); - } - if (!calledBySeek) { - mPlayer.seekTo(mPlayer.getCurrentPosition()); - } - } - } - - /** - * Seeks to the specified position of the current playback. - * - * @param positionMs the specified position in milli seconds. - */ - public void seekTo(long positionMs) { - mPlayer.seekTo(positionMs); - stopSmoothTrickplay(true); - } - - /** - * Releases the player. - */ - public void release() { - if (mDataSource != null) { - mSourceManager.releaseDataSource(mDataSource); - mDataSource = null; - } - if (mBuilderCallback != null) { - mBuilderCallback.cancel(); - mBuilderCallback = null; - } - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - mSurface = null; - mListener = null; - mPlayer.release(); - } - - /** - * Returns the current status of the player. - */ - public int getPlaybackState() { - if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { - return ExoPlayer.STATE_PREPARING; - } - return mPlayer.getPlaybackState(); - } - - /** - * Returns {@code true} when the player is prepared to play, {@code false} otherwise. - */ - public boolean isPrepared() { - int state = getPlaybackState(); - return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING; - } - - /** - * Returns {@code true} when the player is being ready to play, {@code false} otherwise. - */ - public boolean isPlaying() { - int state = getPlaybackState(); - return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) - && mPlayer.getPlayWhenReady(); - } - - /** - * Returns {@code true} when the player is buffering, {@code false} otherwise. - */ - public boolean isBuffering() { - return getPlaybackState() == ExoPlayer.STATE_BUFFERING; - } - - /** - * Returns the current position of the playback in milli seconds. - */ - public long getCurrentPosition() { - return mPlayer.getCurrentPosition(); - } - - /** - * Returns the total duration of the playback. - */ - public long getDuration() { - return mPlayer.getDuration(); - } - - /** - * Returns {@code true} when the player is being ready to play, - * {@code false} when the player is paused. - */ - public boolean getPlayWhenReady() { - return mPlayer.getPlayWhenReady(); - } - - /** - * Sets the volume of the audio. - * - * @param volume see also {@link AudioTrack#setVolume(float)} - */ - public void setVolume(float volume) { - mVolume = volume; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, - volume); - } else { - mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, - volume); - } - } - - /** - * Enables or disables audio and closed caption. - * - * @param enable enables the audio and closed caption when {@code true}, disables otherwise. - */ - public void setAudioTrackAndClosedCaption(boolean enable) { - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK, - enable ? 1 : 0); - } else { - mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, - enable ? mVolume : 0.0f); - } - mPlayer.sendMessage(mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION, - enable); - } - - /** - * Returns {@code true} when AC3 audio can be played, {@code false} otherwise. - */ - public boolean isAc3Playable() { - return mAudioCapabilities != null - && mAudioCapabilities.supportsEncoding(AudioFormat.ENCODING_AC3); - } - - /** - * Notifies when the audio cannot be played by the current device. - */ - public void onAudioUnplayable() { - if (mListener != null) { - mListener.onAudioUnplayable(); - } - } - - /** - * Returns {@code true} if the player has any video track, {@code false} otherwise. - */ - public boolean hasVideo() { - return mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0; - } - - /** - * Returns {@code true} if the player has any audio trock, {@code false} otherwise. - */ - public boolean hasAudio() { - return mPlayer.getTrackCount(TRACK_TYPE_AUDIO) > 0; - } - - /** - * Returns the number of tracks exposed by the specified renderer. - */ - public int getTrackCount(int rendererIndex) { - return mPlayer.getTrackCount(rendererIndex); - } - - /** - * Selects a track for the specified renderer. - */ - public void setSelectedTrack(int rendererIndex, int trackIndex) { - if (trackIndex >= getTrackCount(rendererIndex)) { - return; - } - mPlayer.setSelectedTrack(rendererIndex, trackIndex); - } - - /** - * Returns the index of the currently selected track for the specified renderer. - * - * @param rendererIndex The index of the renderer. - * @return The selected track. A negative value or a value greater than or equal to the renderer's - * track count indicates that the renderer is disabled. - */ - public int getSelectedTrack(int rendererIndex) { - return mPlayer.getSelectedTrack(rendererIndex); - } - - /** - * Returns the format of a track. - * - * @param rendererIndex The index of the renderer. - * @param trackIndex The index of the track. - * @return The format of the track. - */ - public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { - return mPlayer.getTrackFormat(rendererIndex, trackIndex); - } - - /** - * Gets the main handler of the player. - */ - /* package */ Handler getMainHandler() { - return mMainHandler; - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - if (mListener == null) { - return; - } - mListener.onStateChanged(playWhenReady, state); - if (state == ExoPlayer.STATE_READY && mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0 - && playWhenReady) { - MediaFormat format = mPlayer.getTrackFormat(TRACK_TYPE_VIDEO, 0); - mListener.onVideoSizeChanged(format.width, - format.height, format.pixelWidthHeightRatio); - } - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - if (mListener != null) { - mListener.onError(exception); - } - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - if (mListener != null) { - mListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); - } - } - - @Override - public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - // Do nothing. - } - - @Override - public void onDecoderInitializationError(DecoderInitializationException e) { - // Do nothing. - } - - @Override - public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { - if (mListener != null) { - mListener.onAudioUnplayable(); - } - } - - @Override - public void onAudioTrackWriteError(AudioTrack.WriteException e) { - // Do nothing. - } - - @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { - // Do nothing. - } - - @Override - public void onCryptoError(CryptoException e) { - // Do nothing. - } - - @Override - public void onPlayWhenReadyCommitted() { - // Do nothing. - } - - @Override - public void onDrawnToSurface(Surface surface) { - if (mListener != null) { - mListener.onDrawnToSurface(this, surface); - } - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - TunerDebug.notifyVideoFrameDrop(count, elapsed); - if (mTrickplayRunning && mListener != null) { - mListener.onSmoothTrickplayForceStopped(); - } - } - - @Override - public void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (mTrickplayRunning && mListener != null) { - mListener.onSmoothTrickplayForceStopped(); - } - } - - private void pushSurface(boolean blockForSurfacePush) { - if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { - return; - } - - if (blockForSurfacePush) { - mPlayer.blockingSendMessage( - mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); - } else { - mPlayer.sendMessage( - mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); - } - } - - private void pushTrackSelection(@TrackType int type, boolean allowRendererEnable) { - if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { - return; - } - mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1); - } - - private class MpegTsCcListener implements Cea708TextTrackRenderer.CcListener { - - @Override - public void emitEvent(CaptionEvent captionEvent) { - if (mVideoEventListener != null) { - mVideoEventListener.onEmitCaptionEvent(captionEvent); - } - } - - @Override - public void clearCaption() { - if (mVideoEventListener != null) { - mVideoEventListener.onClearCaptionEvent(); - } - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - if (mVideoEventListener != null) { - mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber); - } - } - } - - private class InternalRendererBuilderCallback implements RendererBuilderCallback { - private boolean canceled; - - public void cancel() { - canceled = true; - } - - @Override - public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { - if (!canceled) { - MpegTsPlayer.this.onRenderers(renderers); - } - } - - @Override - public void onRenderersError(Exception e) { - if (!canceled) { - MpegTsPlayer.this.onRenderersError(e); - } - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java deleted file mode 100644 index 006ccac2..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.content.Context; - -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.Features; -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; - -/** - * Builder for renderer objects for {@link MpegTsPlayer}. - */ -public class MpegTsRendererBuilder implements RendererBuilder { - private final Context mContext; - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - - public MpegTsRendererBuilder(Context context, BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mContext = context; - mBufferManager = bufferManager; - mBufferListener = bufferListener; - } - - @Override - public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - boolean mHasSoftwareAudioDecoder, RendererBuilderCallback callback) { - // Build the video and audio renderers. - SampleExtractor extractor = dataSource == null ? - new MpegTsSampleExtractor(mBufferManager, mBufferListener) : - new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener); - SampleSource sampleSource = new MpegTsSampleSource(extractor); - MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, - sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); - // TODO: Only using MpegTsDefaultAudioTrackRenderer for A/V sync issue. We will use - // {@link MpegTsMediaCodecAudioTrackRenderer} when we use ExoPlayer's extractor. - TrackRenderer audioRenderer = - new MpegTsDefaultAudioTrackRenderer( - sampleSource, - MediaCodecSelector.DEFAULT, - mpegTsPlayer.getMainHandler(), - mpegTsPlayer, - mHasSoftwareAudioDecoder, - !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext)); - Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); - - TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; - renderers[MpegTsPlayer.TRACK_TYPE_VIDEO] = videoRenderer; - renderers[MpegTsPlayer.TRACK_TYPE_AUDIO] = audioRenderer; - renderers[MpegTsPlayer.TRACK_TYPE_TEXT] = textRenderer; - callback.onRenderers(null, renderers); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java deleted file mode 100644 index 7bf116c8..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.net.Uri; -import android.os.Handler; - -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.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.SamplePool; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * Extracts samples from {@link DataSource} for MPEG-TS streams. - */ -public final class MpegTsSampleExtractor implements SampleExtractor { - public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; - - private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8; - - private final SampleExtractor mSampleExtractor; - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - private final List<Boolean> mReachedEos = new ArrayList<>(); - private int mVideoTrackIndex; - private final SamplePool mCcSamplePool = new SamplePool(); - private final List<SampleHolder> mPendingCcSamples = new LinkedList<>(); - - private int mCea708TextTrackIndex; - private boolean mCea708TextTrackSelected; - - private CcParser mCcParser; - - private void init() { - mVideoTrackIndex = -1; - mCea708TextTrackIndex = -1; - mCea708TextTrackSelected = false; - } - - /** - * Creates MpegTsSampleExtractor for {@link DataSource}. - * - * @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 - */ - public MpegTsSampleExtractor(DataSource source, BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mSampleExtractor = new ExoPlayerSampleExtractor(Uri.EMPTY, source, bufferManager, - bufferListener, false); - init(); - } - - /** - * Creates MpegTsSampleExtractor for a recorded program. - * - * @param bufferManager the samples provider which is stored in physical storage - * @param bufferListener the {@link PlaybackBufferListener} - * to notify buffer storage status change - */ - public MpegTsSampleExtractor(BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener); - init(); - } - - @Override - public void maybeThrowError() throws IOException { - if (mSampleExtractor != null) { - mSampleExtractor.maybeThrowError(); - } - } - - @Override - public boolean prepare() throws IOException { - if(!mSampleExtractor.prepare()) { - return false; - } - List<MediaFormat> formats = mSampleExtractor.getTrackFormats(); - int trackCount = formats.size(); - mTrackFormats.clear(); - mReachedEos.clear(); - - for (int i = 0; i < trackCount; ++i) { - mTrackFormats.add(formats.get(i)); - mReachedEos.add(false); - String mime = formats.get(i).mimeType; - if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) { - mVideoTrackIndex = i; - if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) { - mCcParser = new Mpeg2CcParser(); - } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { - mCcParser = new H264CcParser(); - } - } - } - - if (mVideoTrackIndex != -1) { - mCea708TextTrackIndex = trackCount; - } - if (mCea708TextTrackIndex >= 0) { - mTrackFormats.add(MediaFormat.createTextFormat(null, MIMETYPE_TEXT_CEA_708, 0, - mTrackFormats.get(0).durationUs, "")); - } - return true; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void selectTrack(int index) { - if (index == mCea708TextTrackIndex) { - mCea708TextTrackSelected = true; - return; - } - mSampleExtractor.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - if (index == mCea708TextTrackIndex) { - mCea708TextTrackSelected = false; - return; - } - mSampleExtractor.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleExtractor.getBufferedPositionUs(); - } - - @Override - public void seekTo(long positionUs) { - mSampleExtractor.seekTo(positionUs); - for (SampleHolder holder : mPendingCcSamples) { - mCcSamplePool.releaseSample(holder); - } - mPendingCcSamples.clear(); - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - if (track != mCea708TextTrackIndex) { - mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder); - } - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - if (track == mCea708TextTrackIndex) { - if (mCea708TextTrackSelected && !mPendingCcSamples.isEmpty()) { - SampleHolder holder = mPendingCcSamples.remove(0); - holder.data.flip(); - sampleHolder.timeUs = holder.timeUs; - sampleHolder.data.put(holder.data); - mCcSamplePool.releaseSample(holder); - return SampleSource.SAMPLE_READ; - } else { - return mVideoTrackIndex < 0 || mReachedEos.get(mVideoTrackIndex) - ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ; - } - } - - int result = mSampleExtractor.readSample(track, sampleHolder); - switch (result) { - case SampleSource.END_OF_STREAM: { - mReachedEos.set(track, true); - break; - } - case SampleSource.SAMPLE_READ: { - if (mCea708TextTrackSelected && track == mVideoTrackIndex - && sampleHolder.data != null) { - mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs); - } - break; - } - } - return result; - } - - @Override - public void release() { - mSampleExtractor.release(); - mVideoTrackIndex = -1; - mCea708TextTrackIndex = -1; - mCea708TextTrackSelected = false; - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleExtractor.continueBuffering(positionUs); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { } - - private abstract class CcParser { - // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using - // relatively small buffer size in order to minimize memory footprint increase. - protected final byte[] mBuffer = new byte[1024]; - - abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs); - - protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { - // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9. - int pos = offset; - if (pos + 2 >= buffer.position()) { - return offset; - } - boolean processCcDataFlag = (buffer.get(pos) & 64) != 0; - int ccCount = buffer.get(pos) & 0x1f; - pos += 2; - if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) { - return offset; - } - SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES); - for (int i = 0; i < 3 * ccCount; i++) { - holder.data.put(buffer.get(pos++)); - } - holder.timeUs = presentationTimeUs; - mPendingCcSamples.add(holder); - return pos; - } - } - - private class Mpeg2CcParser extends CcParser { - private static final int PATTERN_LENGTH = 9; - - @Override - public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int totalSize = buffer.position(); - // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with - // overlapping to handle the case that the pattern exists in the boundary. - for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { - buffer.position(i); - int size = Math.min(totalSize - i, mBuffer.length); - buffer.get(mBuffer, 0, size); - int j = 0; - while (j < size - PATTERN_LENGTH) { - // Find the start prefix code of private user data. - if (mBuffer[j] == 0 - && mBuffer[j + 1] == 0 - && mBuffer[j + 2] == 1 - && (mBuffer[j + 3] & 0xff) == 0xb2) { - // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user - // identifier and user data type code 3. - if (mBuffer[j + 4] == 'G' - && mBuffer[j + 5] == 'A' - && mBuffer[j + 6] == '9' - && mBuffer[j + 7] == '4' - && mBuffer[j + 8] == 3) { - j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, - presentationTimeUs) - i; - } else { - j += PATTERN_LENGTH; - } - } else { - ++j; - } - } - } - buffer.position(totalSize); - } - } - - private class H264CcParser extends CcParser { - private static final int PATTERN_LENGTH = 14; - - @Override - public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int totalSize = buffer.position(); - // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with - // overlapping to handle the case that the pattern exists in the boundary. - for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { - buffer.position(i); - int size = Math.min(totalSize - i, mBuffer.length); - buffer.get(mBuffer, 0, size); - int j = 0; - while (j < size - PATTERN_LENGTH) { - // Find the start prefix code of a NAL Unit. - if (mBuffer[j] == 0 - && mBuffer[j + 1] == 0 - && mBuffer[j + 2] == 1) { - int nalType = mBuffer[j + 3] & 0x1f; - int payloadType = mBuffer[j + 4] & 0xff; - - // ATSC closed caption data embedded in H264 private user data has NAL type - // 6, payload type 4, and 'GA94' user identifier for ATSC. - if (nalType == 6 && payloadType == 4 && mBuffer[j + 9] == 'G' - && mBuffer[j + 10] == 'A' - && mBuffer[j + 11] == '9' - && mBuffer[j + 12] == '4') { - j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, - presentationTimeUs) - i; - } else { - j += 7; - } - } else { - ++j; - } - } - } - buffer.position(totalSize); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java deleted file mode 100644 index 6007b0be..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.tv.tuner.exoplayer; - -import com.google.android.exoplayer.C; -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.SampleSource.SampleSourceReader; -import com.google.android.exoplayer.util.Assertions; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** {@link SampleSource} that extracts sample data using a {@link SampleExtractor}. */ -public final class MpegTsSampleSource implements SampleSource, SampleSourceReader { - - private static final int TRACK_STATE_DISABLED = 0; - private static final int TRACK_STATE_ENABLED = 1; - private static final int TRACK_STATE_FORMAT_SENT = 2; - - private final SampleExtractor mSampleExtractor; - private final List<Integer> mTrackStates = new ArrayList<>(); - private final List<Boolean> mPendingDiscontinuities = new ArrayList<>(); - - private boolean mPrepared; - private IOException mPreparationError; - private int mRemainingReleaseCount; - - private long mLastSeekPositionUs; - private long mPendingSeekPositionUs; - - /** - * Creates a new sample source that extracts samples using {@code mSampleExtractor}. - * - * @param sampleExtractor a sample extractor for accessing media samples - */ - public MpegTsSampleSource(SampleExtractor sampleExtractor) { - mSampleExtractor = Assertions.checkNotNull(sampleExtractor); - } - - @Override - public SampleSourceReader register() { - mRemainingReleaseCount++; - return this; - } - - @Override - public boolean prepare(long positionUs) { - if (!mPrepared) { - if (mPreparationError != null) { - return false; - } - try { - if (mSampleExtractor.prepare()) { - int trackCount = mSampleExtractor.getTrackFormats().size(); - mTrackStates.clear(); - mPendingDiscontinuities.clear(); - for (int i = 0; i < trackCount; ++i) { - mTrackStates.add(i, TRACK_STATE_DISABLED); - mPendingDiscontinuities.add(i, false); - } - mPrepared = true; - } else { - return false; - } - } catch (IOException e) { - mPreparationError = e; - return false; - } - } - return true; - } - - @Override - public int getTrackCount() { - Assertions.checkState(mPrepared); - return mSampleExtractor.getTrackFormats().size(); - } - - @Override - public MediaFormat getFormat(int track) { - Assertions.checkState(mPrepared); - return mSampleExtractor.getTrackFormats().get(track); - } - - @Override - public void enable(int track, long positionUs) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) == TRACK_STATE_DISABLED); - mTrackStates.set(track, TRACK_STATE_ENABLED); - mSampleExtractor.selectTrack(track); - seekToUsInternal(positionUs, positionUs != 0); - } - - @Override - public void disable(int track) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED); - mSampleExtractor.deselectTrack(track); - mPendingDiscontinuities.set(track, false); - mTrackStates.set(track, TRACK_STATE_DISABLED); - } - - @Override - public boolean continueBuffering(int track, long positionUs) { - return mSampleExtractor.continueBuffering(positionUs); - } - - @Override - public long readDiscontinuity(int track) { - if (mPendingDiscontinuities.get(track)) { - mPendingDiscontinuities.set(track, false); - return mLastSeekPositionUs; - } - return NO_DISCONTINUITY; - } - - @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED); - if (mPendingDiscontinuities.get(track)) { - return NOTHING_READ; - } - if (mTrackStates.get(track) != TRACK_STATE_FORMAT_SENT) { - mSampleExtractor.getTrackMediaFormat(track, formatHolder); - mTrackStates.set(track, TRACK_STATE_FORMAT_SENT); - return FORMAT_READ; - } - - mPendingSeekPositionUs = C.UNKNOWN_TIME_US; - return mSampleExtractor.readSample(track, sampleHolder); - } - - @Override - public void maybeThrowError() throws IOException { - if (mPreparationError != null) { - throw mPreparationError; - } - if (mSampleExtractor != null) { - mSampleExtractor.maybeThrowError(); - } - } - - @Override - public void seekToUs(long positionUs) { - Assertions.checkState(mPrepared); - seekToUsInternal(positionUs, false); - } - - @Override - public long getBufferedPositionUs() { - Assertions.checkState(mPrepared); - return mSampleExtractor.getBufferedPositionUs(); - } - - @Override - public void release() { - Assertions.checkState(mRemainingReleaseCount > 0); - if (--mRemainingReleaseCount == 0) { - mSampleExtractor.release(); - } - } - - private void seekToUsInternal(long positionUs, boolean force) { - // Unless forced, avoid duplicate calls to the underlying extractor's seek method - // in the case that there have been no interleaving calls to readSample. - if (force || mPendingSeekPositionUs != positionUs) { - mLastSeekPositionUs = positionUs; - mPendingSeekPositionUs = positionUs; - mSampleExtractor.seekTo(positionUs); - for (int i = 0; i < mTrackStates.size(); ++i) { - if (mTrackStates.get(i) != TRACK_STATE_DISABLED) { - mPendingDiscontinuities.set(i, true); - } - } - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java deleted file mode 100644 index 19360c69..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.android.tv.tuner.exoplayer; - -import android.content.Context; -import android.media.MediaCodec; -import android.os.Handler; -import android.util.Log; - -import com.google.android.exoplayer.DecoderInfo; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.MediaSoftwareCodecUtil; -import com.google.android.exoplayer.SampleSource; -import com.android.tv.common.feature.CommonFeatures; - -import java.lang.reflect.Field; - -/** - * MPEG-2 TS video track renderer - */ -public class MpegTsVideoTrackRenderer extends MediaCodecVideoTrackRenderer { - private static final String TAG = "MpegTsVideoTrackRender"; - - private static final int VIDEO_PLAYBACK_DEADLINE_IN_MS = 5000; - // If DROPPED_FRAMES_NOTIFICATION_THRESHOLD frames are consecutively dropped, it'll be notified. - private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10; - private static final int MIN_HD_HEIGHT = 720; - private static final String MIMETYPE_MPEG2 = "video/mpeg2"; - 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 { - sRenderedFirstFrameField = MediaCodecVideoTrackRenderer.class.getDeclaredField( - "renderedFirstFrame"); - sRenderedFirstFrameField.setAccessible(true); - } catch (NoSuchFieldException e) { - // Null-checking for {@code sRenderedFirstFrameField} will do the error handling. - } - } - - public MpegTsVideoTrackRenderer(Context context, SampleSource source, Handler handler, - MediaCodecVideoTrackRenderer.EventListener listener) { - super(context, source, MediaCodecSelector.DEFAULT, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_PLAYBACK_DEADLINE_IN_MS, handler, - listener, DROPPED_FRAMES_NOTIFICATION_THRESHOLD); - mIsSwCodecEnabled = CommonFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context); - } - - @Override - protected DecoderInfo getDecoderInfo(MediaCodecSelector codecSelector, String mimeType, - boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException { - try { - if (mIsSwCodecEnabled && mCodecIsSwPreferred) { - DecoderInfo swCodec = MediaSoftwareCodecUtil.getSoftwareDecoderInfo( - mimeType, requiresSecureDecoder); - if (swCodec != null) { - return swCodec; - } - } - } catch (MediaSoftwareCodecUtil.DecoderQueryException e) { - } - return super.getDecoderInfo(codecSelector, mimeType,requiresSecureDecoder); - } - - @Override - protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { - mCodecIsSwPreferred = MIMETYPE_MPEG2.equalsIgnoreCase(holder.format.mimeType) - && holder.format.height < MIN_HD_HEIGHT; - super.onInputFormatChanged(holder); - } - - @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { - super.onDiscontinuity(positionUs); - // 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/src/com/android/tv/tuner/exoplayer/SampleExtractor.java b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java deleted file mode 100644 index 543588c7..00000000 --- a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.tv.tuner.exoplayer; - -import android.os.Handler; - -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.TrackRenderer; - -import java.io.IOException; -import java.util.List; - -/** - * Extractor for reading track metadata and samples stored in tracks. - * - * <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via - * {@link #getTrackFormats} and {@link #getTrackMediaFormat}. - * - * <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected - * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample - * data or seeking. Initially, all tracks are deselected. - * - * <p>Call {@link #release()} when the extractor is no longer needed to free resources. - */ -public interface SampleExtractor { - - /** - * If the extractor is currently having difficulty preparing or loading samples, then this - * method throws the underlying error. Otherwise does nothing. - * - * @throws IOException The underlying error. - */ - void maybeThrowError() throws IOException; - - /** - * Prepares the extractor for reading track metadata and samples. - * - * @return whether the source is ready; if {@code false}, this method must be called again. - * @throws IOException thrown if the source can't be read - */ - boolean prepare() throws IOException; - - /** Returns track information about all tracks that can be selected. */ - List<MediaFormat> getTrackFormats(); - - /** Selects the track at {@code index} for reading sample data. */ - void selectTrack(int index); - - /** Deselects the track at {@code index}, so no more samples will be read from that track. */ - void deselectTrack(int index); - - /** - * Returns an estimate of the position up to which data is buffered. - * - * <p>This method should not be called until after the extractor has been successfully prepared. - * - * @return an estimate of the absolute position in microseconds up to which data is buffered, - * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or - * {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available. - */ - long getBufferedPositionUs(); - - /** - * Seeks to the specified time in microseconds. - * - * <p>This method should not be called until after the extractor has been successfully prepared. - * - * @param positionUs the seek position in microseconds - */ - void seekTo(long positionUs); - - /** Stores the {@link MediaFormat} of {@code track}. */ - void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder); - - /** - * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning - * {@link SampleSource#SAMPLE_READ} if it is available. - * - * <p>Advances to the next sample if a sample was read. - * - * @param track the index of the track from which to read a sample - * @param sampleHolder the holder for read sample data, if {@link SampleSource#SAMPLE_READ} is - * returned - * @return {@link SampleSource#SAMPLE_READ} if a sample was read into {@code sampleHolder}, or - * {@link SampleSource#END_OF_STREAM} if the last samples in all tracks have been read, or - * {@link SampleSource#NOTHING_READ} if the sample cannot be read immediately as it is not - * loaded. - */ - int readSample(int track, SampleHolder sampleHolder); - - /** Releases resources associated with this extractor. */ - void release(); - - /** Indicates to the source that it should still be buffering data. */ - boolean continueBuffering(long positionUs); - - /** - * Sets OnCompletionListener for notifying the completion of SampleExtractor. - * - * @param listener the OnCompletionListener - * @param handler the {@link Handler} for {@link Handler#post(Runnable)} of OnCompletionListener - */ - void setOnCompletionListener(OnCompletionListener listener, Handler handler); - - /** - * The listener for SampleExtractor being completed. - */ - interface OnCompletionListener { - - /** - * Called when sample extraction is completed. - * - * @param result {@code true} when the extractor is finished without an error, - * {@code false} otherwise (storage error, weak signal, being reached at EoS - * prematurely, etc.) - * @param lastExtractedPositionUs the last extracted position when extractor is completed - */ - void onCompletion(boolean result, long lastExtractedPositionUs); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java deleted file mode 100644 index 5666c5b9..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.audio; - -import com.android.tv.common.SoftPreconditions; - -import android.os.SystemClock; - -/** - * Copy of {@link com.google.android.exoplayer.MediaClock}. - * <p> - * A simple clock for tracking the progression of media time. The clock can be started, stopped and - * its time can be set and retrieved. When started, this clock is based on - * {@link SystemClock#elapsedRealtime()}. - */ -/* package */ class AudioClock { - private boolean mStarted; - - /** - * The media time when the clock was last set or stopped. - */ - private long mPositionUs; - - /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #mPositionUs} - * when the clock was last set or mStarted. - */ - private long mDeltaUs; - - private float mPlaybackSpeed = 1.0f; - private long mDeltaUpdatedTimeUs; - - /** - * Starts the clock. Does nothing if the clock is already started. - */ - public void start() { - if (!mStarted) { - mStarted = true; - mDeltaUs = elapsedRealtimeMinus(mPositionUs); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - } - } - - /** - * Stops the clock. Does nothing if the clock is already stopped. - */ - public void stop() { - if (mStarted) { - mPositionUs = elapsedRealtimeMinus(mDeltaUs); - mStarted = false; - } - } - - /** - * @param timeUs The position to set in microseconds. - */ - public void setPositionUs(long timeUs) { - this.mPositionUs = timeUs; - mDeltaUs = elapsedRealtimeMinus(timeUs); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - } - - /** - * @return The current position in microseconds. - */ - public long getPositionUs() { - if (!mStarted) { - return mPositionUs; - } - if (mPlaybackSpeed != 1.0f) { - long elapsedTimeFromPlaybackSpeedChanged = SystemClock.elapsedRealtime() * 1000 - - mDeltaUpdatedTimeUs; - return elapsedRealtimeMinus(mDeltaUs) - + (long) ((mPlaybackSpeed - 1.0f) * elapsedTimeFromPlaybackSpeedChanged); - } else { - return elapsedRealtimeMinus(mDeltaUs); - } - } - - /** - * Sets playback speed. {@code speed} should be positive. - */ - public void setPlaybackSpeed(float speed) { - SoftPreconditions.checkState(speed > 0); - mDeltaUs = elapsedRealtimeMinus(getPositionUs()); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - mPlaybackSpeed = speed; - } - - private long elapsedRealtimeMinus(long toSubtractUs) { - return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java deleted file mode 100644 index e581092a..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java +++ /dev/null @@ -1,70 +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.exoplayer.audio; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; - -import java.nio.ByteBuffer; - -/** A base class for audio decoders. */ -public abstract class AudioDecoder { - - /** - * Decodes an audio sample. - * - * @param sampleHolder a holder that contains the sample data and corresponding metadata - */ - public abstract void decode(SampleHolder sampleHolder); - - /** Returns a decoded sample from decoder. */ - public abstract ByteBuffer getDecodedSample(); - - /** Returns the presentation time for the decoded sample. */ - public abstract long getDecodedTimeUs(); - - /** - * Clear previous decode state if any. Prepares to decode samples of the specified encoding. - * This method should be called before using decode. - * - * @param mime audio encoding - */ - public abstract void resetDecoderState(String mimeType); - - /** Releases all the resource. */ - public abstract void release(); - - /** - * Init decoder if needed. - * - * @param format the format used to initialize decoder - */ - public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { - // Do nothing. - } - - /** Returns input buffer that will be used in decoder. */ - public ByteBuffer getInputBuffer() { - return null; - } - - /** Returns the output format. */ - public android.media.MediaFormat getOutputFormat() { - return null; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java deleted file mode 100644 index ec616b13..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.audio; - -import android.os.SystemClock; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.util.MimeTypes; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -/** - * Monitors the rendering position of {@link AudioTrack}. - */ -public class AudioTrackMonitor { - private static final String TAG = "AudioTrackMonitor"; - private static final boolean DEBUG = false; - - // For fetched audio samples - private final ArrayList<Pair<Long, Integer>> mPtsList = new ArrayList<>(); - private final Set<Integer> mSampleSize = new HashSet<>(); - private final Set<Integer> mCurSampleSize = new HashSet<>(); - private final Set<Integer> mHeader = new HashSet<>(); - - private long mExpireMs; - private long mDuration; - private long mSampleCount; - private long mTotalCount; - private long mStartMs; - - private boolean mIsMp2; - - private void flush() { - mExpireMs += mDuration; - mSampleCount = 0; - mCurSampleSize.clear(); - mPtsList.clear(); - } - - /** - * Resets and initializes {@link AudioTrackMonitor}. - * - * @param duration the frequency of monitoring in milliseconds - */ - public void reset(long duration) { - mExpireMs = SystemClock.elapsedRealtime(); - mDuration = duration; - mTotalCount = 0; - mStartMs = 0; - mSampleSize.clear(); - mHeader.clear(); - flush(); - } - - public void setEncoding(String mime) { - mIsMp2 = MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mime); - } - - /** - * Adds an audio sample information for monitoring. - * - * @param pts the presentation timestamp of the sample - * @param sampleSize the size in bytes of the sample - * @param header the bitrate & sampling information header of the sample - */ - public void addPts(long pts, int sampleSize, int header) { - mTotalCount++; - mSampleCount++; - mSampleSize.add(sampleSize); - mHeader.add(header); - mCurSampleSize.add(sampleSize); - if (mTotalCount == 1) { - mStartMs = SystemClock.elapsedRealtime(); - } - if (mPtsList.isEmpty() || mPtsList.get(mPtsList.size() - 1).first != pts) { - mPtsList.add(Pair.create(pts, 1)); - return; - } - Pair<Long, Integer> pair = mPtsList.get(mPtsList.size() - 1); - mPtsList.set(mPtsList.size() - 1, Pair.create(pair.first, pair.second + 1)); - } - - /** - * Logs if interested events are present. - * <p> - * Periodic logging is not enabled in release mode in order to avoid verbose logging. - */ - public void maybeLog() { - long now = SystemClock.elapsedRealtime(); - if (mExpireMs != 0 && now >= mExpireMs) { - if (DEBUG) { - long unitDuration = mIsMp2 ? MpegTsDefaultAudioTrackRenderer.MP2_SAMPLE_DURATION_US - : MpegTsDefaultAudioTrackRenderer.AC3_SAMPLE_DURATION_US; - long sampleDuration = (mTotalCount - 1) * unitDuration / 1000; - long totalDuration = now - mStartMs; - StringBuilder ptsBuilder = new StringBuilder(); - ptsBuilder.append("PTS received ").append(mSampleCount).append(", ") - .append(totalDuration - sampleDuration).append(' '); - - for (Pair<Long, Integer> pair : mPtsList) { - ptsBuilder.append('[').append(pair.first).append(':').append(pair.second) - .append("], "); - } - Log.d(TAG, ptsBuilder.toString()); - } - if (DEBUG || mCurSampleSize.size() > 1) { - Log.d(TAG, "PTS received sample size: " - + String.valueOf(mSampleSize) + mCurSampleSize + mHeader); - } - flush(); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java deleted file mode 100644 index 953c9fc4..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.audio; - -import android.media.MediaFormat; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.audio.AudioTrack; - -import java.nio.ByteBuffer; - -/** - * {@link AudioTrack} wrapper class for trickplay operations including FF/RW. - * FF/RW trickplay operations do not need framework {@link AudioTrack}. - * This wrapper class will do nothing in disabled status for those operations. - */ -public class AudioTrackWrapper { - private static final int PCM16_FRAME_BYTES = 2; - private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536; - private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK = - MpegTsDefaultAudioTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK; - private final AudioTrack mAudioTrack = new AudioTrack(); - private int mAudioSessionID; - private boolean mIsEnabled; - - AudioTrackWrapper() { - mIsEnabled = true; - } - - public void resetSessionId() { - mAudioSessionID = AudioTrack.SESSION_ID_NOT_SET; - } - - public boolean isInitialized() { - return mIsEnabled && mAudioTrack.isInitialized(); - } - - public void restart() { - if (mAudioTrack.isInitialized()) { - mAudioTrack.release(); - } - mIsEnabled = true; - resetSessionId(); - } - - public void release() { - if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) { - mAudioTrack.release(); - } - } - - public void initialize() throws AudioTrack.InitializationException { - if (!mIsEnabled) { - return; - } - if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) { - mAudioTrack.initialize(mAudioSessionID); - } else { - mAudioSessionID = mAudioTrack.initialize(); - } - } - - public void reset() { - if (!mIsEnabled) { - return; - } - mAudioTrack.reset(); - } - - public boolean isEnded() { - return !mIsEnabled || !mAudioTrack.hasPendingData(); - } - - public boolean isReady() { - // In the case of not playing actual audio data, Audio track is always ready. - return !mIsEnabled || mAudioTrack.hasPendingData(); - } - - public void play() { - if (!mIsEnabled) { - return; - } - mAudioTrack.play(); - } - - public void pause() { - if (!mIsEnabled) { - return; - } - mAudioTrack.pause(); - } - - public void setVolume(float volume) { - if (!mIsEnabled) { - return; - } - mAudioTrack.setVolume(volume); - } - - public void reconfigure(MediaFormat format, int audioBufferSize) { - if (!mIsEnabled || format == null) { - return; - } - String mimeType = format.getString(MediaFormat.KEY_MIME); - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - int pcmEncoding; - try { - pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING); - } catch (Exception e) { - pcmEncoding = C.ENCODING_PCM_16BIT; - } - // TODO: Handle non-AC3. - if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) { - // Workarounds b/25955476. - // Since all devices and platforms does not support passthrough for non-stereo AC3, - // It is safe to fake non-stereo AC3 as AC3 stereo which is default passthrough mode. - // In other words, the channel count should be always 2. - channelCount = 2; - } - if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) { - audioBufferSize = - channelCount - * PCM16_FRAME_BYTES - * AC3_FRAMES_IN_ONE_SAMPLE - * BUFFERED_SAMPLES_IN_AUDIOTRACK; - } - mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize); - } - - public void handleDiscontinuity() { - if (!mIsEnabled) { - return; - } - mAudioTrack.handleDiscontinuity(); - } - - public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs) - throws AudioTrack.WriteException { - if (!mIsEnabled) { - return AudioTrack.RESULT_BUFFER_CONSUMED; - } - return mAudioTrack.handleBuffer(buffer, offset, size, presentationTimeUs); - } - - public void setStatus(boolean enable) { - if (enable == mIsEnabled) { - return; - } - mAudioTrack.reset(); - mIsEnabled = enable; - } - - public boolean isEnabled() { - return mIsEnabled; - } - - // This should be used only in case of being enabled. - public long getCurrentPositionUs(boolean isEnded) { - return mAudioTrack.getCurrentPositionUs(isEnded); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java deleted file mode 100644 index 72bc68b6..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java +++ /dev/null @@ -1,235 +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.exoplayer.audio; - -import android.media.MediaCodec; -import android.util.Log; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.DecoderInfo; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; - -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** A decoder to use MediaCodec for decoding audio stream. */ -public class MediaCodecAudioDecoder extends AudioDecoder { - private static final String TAG = "MediaCodecAudioDecoder"; - - public static final int INDEX_INVALID = -1; - - private final CodecCounters mCodecCounters; - private final MediaCodecSelector mSelector; - - private MediaCodec mCodec; - private MediaCodec.BufferInfo mOutputBufferInfo; - private ByteBuffer mMediaCodecOutputBuffer; - private ArrayList<Long> mDecodeOnlyPresentationTimestamps; - private boolean mWaitingForFirstSyncFrame; - private boolean mIsNewIndex; - private int mInputIndex; - private int mOutputIndex; - - /** Creates a MediaCodec based audio decoder. */ - public MediaCodecAudioDecoder(MediaCodecSelector selector) { - mSelector = selector; - mOutputBufferInfo = new MediaCodec.BufferInfo(); - mCodecCounters = new CodecCounters(); - mDecodeOnlyPresentationTimestamps = new ArrayList<>(); - } - - /** Returns {@code true} if there is decoder for {@code mimeType}. */ - public static boolean supportMimeType(MediaCodecSelector selector, String mimeType) { - if (selector == null) { - return false; - } - return getDecoderInfo(selector, mimeType) != null; - } - - private static DecoderInfo getDecoderInfo(MediaCodecSelector selector, String mimeType) { - try { - return selector.getDecoderInfo(mimeType, false); - } catch (MediaCodecUtil.DecoderQueryException e) { - Log.e(TAG, "Select decoder error:" + e); - return null; - } - } - - private boolean shouldInitCodec(MediaFormat format) { - return format != null && mCodec == null; - } - - @Override - public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { - if (!shouldInitCodec(format)) { - return; - } - - String mimeType = format.mimeType; - DecoderInfo decoderInfo = getDecoderInfo(mSelector, mimeType); - if (decoderInfo == null) { - Log.i(TAG, "There is not decoder found for " + mimeType); - return; - } - - String codecName = decoderInfo.name; - try { - mCodec = MediaCodec.createByCodecName(codecName); - mCodec.configure(format.getFrameworkMediaFormatV16(), null, null, 0); - mCodec.start(); - } catch (Exception e) { - Log.e(TAG, "Failed when configure or start codec:" + e); - throw new ExoPlaybackException(e); - } - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mWaitingForFirstSyncFrame = true; - mCodecCounters.codecInitCount++; - } - - @Override - public void resetDecoderState(String mimeType) { - if (mCodec == null) { - return; - } - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mDecodeOnlyPresentationTimestamps.clear(); - mCodec.flush(); - mWaitingForFirstSyncFrame = true; - } - - @Override - public void release() { - if (mCodec != null) { - mDecodeOnlyPresentationTimestamps.clear(); - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mCodecCounters.codecReleaseCount++; - try { - mCodec.stop(); - } finally { - try { - mCodec.release(); - } finally { - mCodec = null; - } - } - } - } - - /** Returns the index of input buffer which is ready for using. */ - public int getInputIndex() { - return mInputIndex; - } - - @Override - public ByteBuffer getInputBuffer() { - if (mInputIndex < 0) { - mInputIndex = mCodec.dequeueInputBuffer(0); - if (mInputIndex < 0) { - return null; - } - return mCodec.getInputBuffer(mInputIndex); - } - return mCodec.getInputBuffer(mInputIndex); - } - - @Override - public void decode(SampleHolder sampleHolder) { - if (mWaitingForFirstSyncFrame) { - if (!sampleHolder.isSyncFrame()) { - sampleHolder.clearData(); - return; - } - mWaitingForFirstSyncFrame = false; - } - long presentationTimeUs = sampleHolder.timeUs; - if (sampleHolder.isDecodeOnly()) { - mDecodeOnlyPresentationTimestamps.add(presentationTimeUs); - } - mCodec.queueInputBuffer(mInputIndex, 0, sampleHolder.data.limit(), presentationTimeUs, 0); - mInputIndex = INDEX_INVALID; - mCodecCounters.inputBufferCount++; - } - - private int getDecodeOnlyIndex(long presentationTimeUs) { - final int size = mDecodeOnlyPresentationTimestamps.size(); - for (int i = 0; i < size; i++) { - if (mDecodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) { - return i; - } - } - return INDEX_INVALID; - } - - /** Returns the index of output buffer which is ready for using. */ - public int getOutputIndex() { - if (mOutputIndex < 0) { - mOutputIndex = mCodec.dequeueOutputBuffer(mOutputBufferInfo, 0); - mIsNewIndex = true; - } else { - mIsNewIndex = false; - } - return mOutputIndex; - } - - @Override - public android.media.MediaFormat getOutputFormat() { - return mCodec.getOutputFormat(); - } - - /** Returns {@code true} if the output is only for decoding but not for rendering. */ - public boolean maybeDecodeOnlyIndex() { - int decodeOnlyIndex = getDecodeOnlyIndex(mOutputBufferInfo.presentationTimeUs); - if (decodeOnlyIndex != INDEX_INVALID) { - mCodec.releaseOutputBuffer(mOutputIndex, false); - mCodecCounters.skippedOutputBufferCount++; - mDecodeOnlyPresentationTimestamps.remove(decodeOnlyIndex); - mOutputIndex = INDEX_INVALID; - return true; - } - return false; - } - - @Override - public ByteBuffer getDecodedSample() { - if (maybeDecodeOnlyIndex() || mOutputIndex < 0) { - return null; - } - if (mIsNewIndex) { - mMediaCodecOutputBuffer = mCodec.getOutputBuffer(mOutputIndex); - } - return mMediaCodecOutputBuffer; - } - - @Override - public long getDecodedTimeUs() { - return mOutputBufferInfo.presentationTimeUs; - } - - /** Releases the output buffer after rendering. */ - public void releaseOutputBuffer() { - mCodecCounters.renderedOutputBufferCount++; - mCodec.releaseOutputBuffer(mOutputIndex, false); - mOutputIndex = INDEX_INVALID; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java deleted file mode 100644 index 77170419..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java +++ /dev/null @@ -1,735 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.audio; - -import android.media.MediaCodec; -import android.os.Build; -import android.os.Handler; -import android.os.SystemClock; -import android.util.Log; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaClock; -import com.google.android.exoplayer.MediaCodecSelector; -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.TrackRenderer; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.MimeTypes; -import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient; -import com.android.tv.tuner.tvinput.TunerDebug; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** - * Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and - * ffmpeg based software decoding (AC3, MP2). - */ -public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements MediaClock { - public static final int MSG_SET_VOLUME = 10000; - public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1; - public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2; - - // ATSC/53 allows sample rate to be only 48Khz. - // One AC3 sample has 1536 frames, and its duration is 32ms. - public static final long AC3_SAMPLE_DURATION_US = 32000; - - // TODO: Check whether DVB broadcasting uses sample rate other than 48Khz. - // MPEG-1 audio Layer II and III has 1152 frames per sample. - // 1152 frames duration is 24ms when sample rate is 48Khz. - static final long MP2_SAMPLE_DURATION_US = 24000; - - // This is around 150ms, 150ms is big enough not to under-run AudioTrack, - // and 150ms is also small enough to fill the buffer rapidly. - static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5; - public static final long INITIAL_AUDIO_BUFFERING_TIME_US = - BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US; - - - private static final String TAG = "MpegTsDefaultAudioTrac"; - private static final boolean DEBUG = false; - - /** - * Interface definition for a callback to be notified of - * {@link com.google.android.exoplayer.audio.AudioTrack} error. - */ - public interface EventListener { - void onAudioTrackInitializationError(AudioTrack.InitializationException e); - void onAudioTrackWriteError(AudioTrack.WriteException e); - } - - private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2; - private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; - private static final int MONITOR_DURATION_MS = 1000; - private static final int AC3_HEADER_BITRATE_OFFSET = 4; - private static final int MP2_HEADER_BITRATE_OFFSET = 2; - private static final int MP2_HEADER_BITRATE_MASK = 0xfc; - - // Keep this as static in order to prevent new framework AudioTrack creation - // while old AudioTrack is being released. - private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper(); - private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000; - - // Ignore AudioTrack backward movement if duration of movement is below the threshold. - private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000; - - // AudioTrack position cannot go ahead beyond this limit. - private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000; - - // Since MediaCodec processing and AudioTrack playing add delay, - // PTS interpolated time should be delayed reasonably when AudioTrack is not used. - private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; - - private final MediaCodecSelector mSelector; - - private final CodecCounters mCodecCounters; - private final SampleSource.SampleSourceReader mSource; - private final MediaFormatHolder mFormatHolder; - private final EventListener mEventListener; - 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; - private String mDecodingMime; - private boolean mFormatConfigured; - private int mSampleSize; - private final ByteBuffer mOutputBuffer; - private AudioDecoder mAudioDecoder; - private boolean mOutputReady; - private int mTrackIndex; - private boolean mSourceStateReady; - private boolean mInputStreamEnded; - private boolean mOutputStreamEnded; - private long mEndOfStreamMs; - private long mCurrentPositionUs; - private int mPresentationCount; - private long mPresentationTimeUs; - private long mInterpolatedTimeUs; - private long mPreviousPositionUs; - private boolean mIsStopped; - private boolean mEnabled = true; - private boolean mIsMuted; - private ArrayList<Integer> mTracksIndex; - private boolean mUseFrameworkDecoder; - - public MpegTsDefaultAudioTrackRenderer( - SampleSource source, - MediaCodecSelector selector, - Handler eventHandler, - EventListener listener, - boolean hasSoftwareAudioDecoder, - boolean usePassthrough) { - mSource = source.register(); - mSelector = selector; - mEventHandler = eventHandler; - mEventListener = listener; - mTrackIndex = -1; - mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); - mFormatHolder = new MediaFormatHolder(); - AUDIO_TRACK.restart(); - mCodecCounters = new CodecCounters(); - mMonitor = new AudioTrackMonitor(); - mAudioClock = new AudioClock(); - mTracksIndex = new ArrayList<>(); - mAc3Passthrough = usePassthrough; - mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable(); - } - - @Override - protected MediaClock getMediaClock() { - return this; - } - - private boolean handlesMimeType(String mimeType) { - return mimeType.equals(MimeTypes.AUDIO_AC3) - || mimeType.equals(MimeTypes.AUDIO_E_AC3) - || mimeType.equals(MimeTypes.AUDIO_MPEG_L2) - || MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); - } - - @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return false; - } - for (int i = 0; i < mSource.getTrackCount(); i++) { - String mimeType = mSource.getFormat(i).mimeType; - if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) { - if (mTrackIndex < 0) { - mTrackIndex = i; - } - mTracksIndex.add(i); - } - } - - // TODO: Check this case. Source does not have the proper mime type. - return true; - } - - @Override - protected int getTrackCount() { - return mTracksIndex.size(); - } - - @Override - protected MediaFormat getFormat(int track) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - return mSource.getFormat(mTracksIndex.get(track)); - } - - @Override - protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - mTrackIndex = mTracksIndex.get(track); - mSource.enable(mTrackIndex, positionUs); - seekToInternal(positionUs); - } - - @Override - protected void onDisabled() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - AUDIO_TRACK.resetSessionId(); - } - clearDecodeState(); - mFormat = null; - mSource.disable(mTrackIndex); - } - - @Override - protected void onReleased() { - releaseDecoder(); - AUDIO_TRACK.release(); - mSource.release(); - } - - @Override - protected boolean isEnded() { - return mOutputStreamEnded && AUDIO_TRACK.isEnded(); - } - - @Override - protected boolean isReady() { - return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady)); - } - - private void seekToInternal(long positionUs) { - mMonitor.reset(MONITOR_DURATION_MS); - mSourceStateReady = false; - mInputStreamEnded = false; - mOutputStreamEnded = false; - mPresentationTimeUs = positionUs; - mPresentationCount = 0; - mPreviousPositionUs = 0; - mCurrentPositionUs = Long.MIN_VALUE; - mInterpolatedTimeUs = Long.MIN_VALUE; - mAudioClock.setPositionUs(positionUs); - } - - @Override - protected void seekTo(long positionUs) { - mSource.seekToUs(positionUs); - AUDIO_TRACK.reset(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - // resetSessionId() will create a new framework AudioTrack instead of reusing old one. - AUDIO_TRACK.resetSessionId(); - } - seekToInternal(positionUs); - clearDecodeState(); - } - - @Override - protected void onStarted() { - AUDIO_TRACK.play(); - mAudioClock.start(); - mIsStopped = false; - } - - @Override - protected void onStopped() { - AUDIO_TRACK.pause(); - mAudioClock.stop(); - mIsStopped = true; - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - try { - mSource.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - mMonitor.maybeLog(); - try { - if (mEndOfStreamMs != 0) { - // Ensure playback stops, after EoS was notified. - // Sometimes MediaCodecTrackRenderer does not fetch EoS timely - // after EoS was notified here long before. - long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs; - if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) { - throw new ExoPlaybackException("Much time has elapsed after EoS"); - } - } - boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); - if (mSourceStateReady != continueBuffering) { - mSourceStateReady = continueBuffering; - if (DEBUG) { - Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady)); - } - } - long discontinuity = mSource.readDiscontinuity(mTrackIndex); - if (discontinuity != SampleSource.NO_DISCONTINUITY) { - AUDIO_TRACK.handleDiscontinuity(); - mPresentationTimeUs = discontinuity; - mPresentationCount = 0; - clearDecodeState(); - return; - } - if (mFormat == null) { - readFormat(); - return; - } - - if (mAudioDecoder != null) { - mAudioDecoder.maybeInitDecoder(mFormat); - } - // Process only one sample at a time for doSomeWork() when using FFmpeg decoder. - if (processOutput()) { - if (!mOutputReady) { - while (feedInputBuffer()) { - if (mOutputReady) break; - } - } - } - mCodecCounters.ensureUpdated(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - private void ensureAudioTrackInitialized() { - if (!AUDIO_TRACK.isInitialized()) { - try { - if (DEBUG) { - Log.d(TAG, "AudioTrack initialized"); - } - AUDIO_TRACK.initialize(); - } catch (AudioTrack.InitializationException e) { - Log.e(TAG, "Error on AudioTrack initialization", e); - notifyAudioTrackInitializationError(e); - - // Do not throw exception here but just disabling audioTrack to keep playing - // video without audio. - AUDIO_TRACK.setStatus(false); - } - if (getState() == TrackRenderer.STATE_STARTED) { - if (DEBUG) { - Log.d(TAG, "AudioTrack played"); - } - AUDIO_TRACK.play(); - } - } - } - - private void clearDecodeState() { - mOutputReady = false; - if (mAudioDecoder != null) { - mAudioDecoder.resetDecoderState(mDecodingMime); - } - AUDIO_TRACK.reset(); - } - - private void releaseDecoder() { - if (mAudioDecoder != null) { - mAudioDecoder.release(); - } - } - - private void readFormat() throws IOException, ExoPlaybackException { - int result = mSource.readData(mTrackIndex, mCurrentPositionUs, - mFormatHolder, mSampleHolder); - if (result == SampleSource.FORMAT_READ) { - onInputFormatChanged(mFormatHolder); - } - } - - 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); - if (mUseFrameworkDecoder) { - mAudioDecoder = new MediaCodecAudioDecoder(mSelector); - mFormat = formatHolder.format; - mAudioDecoder.maybeInitDecoder(mFormat); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); - } else if (mSoftwareDecoderAvailable - && (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mimeType) - || MimeTypes.AUDIO_AC3.equalsIgnoreCase(mimeType) && !mAc3Passthrough)) { - releaseDecoder(); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mAudioDecoder = FfmpegDecoderClient.getInstance(); - mDecodingMime = mimeType; - mFormat = convertMediaFormatToRaw(formatHolder.format); - } else { - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mFormat = formatHolder.format; - releaseDecoder(); - } - mFormatConfigured = true; - mMonitor.setEncoding(mimeType); - if (DEBUG && !mUseFrameworkDecoder) { - Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); - } - clearDecodeState(); - if (!mUseFrameworkDecoder) { - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0); - } - } - - private void onSampleSizeChanged(int sampleSize) { - if (DEBUG) { - Log.d(TAG, "Sample size was changed to : " + sampleSize); - } - clearDecodeState(); - int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK; - mSampleSize = sampleSize; - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize); - } - - private void onOutputFormatChanged(android.media.MediaFormat format) { - if (DEBUG) { - Log.d(TAG, "AudioTrack was configured to FORMAT: " + format.toString()); - } - AUDIO_TRACK.reconfigure(format, 0); - } - - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { - if (mInputStreamEnded) { - return false; - } - - if (mUseFrameworkDecoder) { - boolean indexChanged = - ((MediaCodecAudioDecoder) mAudioDecoder).getInputIndex() - == MediaCodecAudioDecoder.INDEX_INVALID; - if (indexChanged) { - mSampleHolder.data = mAudioDecoder.getInputBuffer(); - if (mSampleHolder.data != null) { - mSampleHolder.clearData(); - } else { - return false; - } - } - } else { - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - } - int result = - mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder); - switch (result) { - case SampleSource.NOTHING_READ: { - return false; - } - case SampleSource.FORMAT_READ: { - Log.i(TAG, "Format was read again"); - onInputFormatChanged(mFormatHolder); - return true; - } - case SampleSource.END_OF_STREAM: { - Log.i(TAG, "End of stream from SampleSource"); - mInputStreamEnded = true; - return false; - } - default: { - if (mSampleHolder.size != mSampleSize - && mFormatConfigured - && !mUseFrameworkDecoder) { - onSampleSizeChanged(mSampleHolder.size); - } - mSampleHolder.data.flip(); - if (!mUseFrameworkDecoder) { - if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { - mMonitor.addPts( - mSampleHolder.timeUs, - mOutputBuffer.position(), - mSampleHolder.data.get(MP2_HEADER_BITRATE_OFFSET) - & MP2_HEADER_BITRATE_MASK); - } else { - mMonitor.addPts( - mSampleHolder.timeUs, - mOutputBuffer.position(), - mSampleHolder.data.get(AC3_HEADER_BITRATE_OFFSET) & 0xff); - } - } - if (mAudioDecoder != null) { - mAudioDecoder.decode(mSampleHolder); - if (mUseFrameworkDecoder) { - int outputIndex = - ((MediaCodecAudioDecoder) mAudioDecoder).getOutputIndex(); - if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - onOutputFormatChanged(mAudioDecoder.getOutputFormat()); - return true; - } else if (outputIndex < 0) { - return true; - } - if (((MediaCodecAudioDecoder) mAudioDecoder).maybeDecodeOnlyIndex()) { - AUDIO_TRACK.handleDiscontinuity(); - return true; - } - } - ByteBuffer outputBuffer = mAudioDecoder.getDecodedSample(); - long presentationTimeUs = mAudioDecoder.getDecodedTimeUs(); - decodeDone(outputBuffer, presentationTimeUs); - } else { - decodeDone(mSampleHolder.data, mSampleHolder.timeUs); - } - return true; - } - } - } - - private boolean processOutput() throws ExoPlaybackException { - if (mOutputStreamEnded) { - return false; - } - if (!mOutputReady) { - if (mInputStreamEnded) { - mOutputStreamEnded = true; - mEndOfStreamMs = SystemClock.elapsedRealtime(); - return false; - } - return true; - } - - ensureAudioTrackInitialized(); - int handleBufferResult; - try { - // To reduce discontinuity, interpolate presentation time. - if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { - mInterpolatedTimeUs = mPresentationTimeUs - + mPresentationCount * MP2_SAMPLE_DURATION_US; - } else if (!mUseFrameworkDecoder) { - mInterpolatedTimeUs = mPresentationTimeUs - + mPresentationCount * AC3_SAMPLE_DURATION_US; - } else { - mInterpolatedTimeUs = mPresentationTimeUs; - } - handleBufferResult = - AUDIO_TRACK.handleBuffer( - mOutputBuffer, 0, mOutputBuffer.limit(), mInterpolatedTimeUs); - } catch (AudioTrack.WriteException e) { - notifyAudioTrackWriteError(e); - throw new ExoPlaybackException(e); - } - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - Log.i(TAG, "Play discontinuity happened"); - mCurrentPositionUs = Long.MIN_VALUE; - } - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { - mCodecCounters.renderedOutputBufferCount++; - mOutputReady = false; - if (mUseFrameworkDecoder) { - ((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer(); - } - return true; - } - return false; - } - - @Override - protected long getDurationUs() { - return mSource.getFormat(mTrackIndex).durationUs; - } - - @Override - protected long getBufferedPositionUs() { - long pos = mSource.getBufferedPositionUs(); - return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US - ? pos : Math.max(pos, getPositionUs()); - } - - @Override - public long getPositionUs() { - if (!AUDIO_TRACK.isInitialized()) { - return mAudioClock.getPositionUs(); - } else if (!AUDIO_TRACK.isEnabled()) { - if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) { - return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; - } - return mPresentationTimeUs; - } - long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded()); - if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { - mPreviousPositionUs = 0L; - if (DEBUG) { - long oldPositionUs = Math.max(mCurrentPositionUs, 0); - long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - Log.d(TAG, "Audio position is not set, diff in us: " - + String.valueOf(currentPositionUs - oldPositionUs)); - } - mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - } else { - if (mPreviousPositionUs - > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) { - Log.e(TAG, "audio_position BACK JUMP: " - + (mPreviousPositionUs - audioTrackCurrentPositionUs)); - mCurrentPositionUs = audioTrackCurrentPositionUs; - } else { - mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs); - } - mPreviousPositionUs = audioTrackCurrentPositionUs; - } - long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US; - if (mCurrentPositionUs > upperBound) { - mCurrentPositionUs = upperBound; - } - return mCurrentPositionUs; - } - - private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) { - if (outputBuffer == null || mOutputBuffer == null) { - return; - } - if (presentationTimeUs < 0) { - Log.e(TAG, "decodeDone - invalid presentationTimeUs"); - return; - } - - if (TunerDebug.ENABLED) { - TunerDebug.setAudioPtsUs(presentationTimeUs); - } - - mOutputBuffer.clear(); - Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); - - mOutputBuffer.put(outputBuffer); - if (presentationTimeUs == mPresentationTimeUs) { - mPresentationCount++; - } else { - mPresentationCount = 0; - mPresentationTimeUs = presentationTimeUs; - } - mOutputBuffer.flip(); - mOutputReady = true; - } - - private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { - if (mEventHandler == null || mEventListener == null) { - return; - } - mEventHandler.post(new Runnable() { - @Override - public void run() { - 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); - } - }); - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_SET_VOLUME: - float volume = (Float) message; - // Workaround: we cannot mute the audio track by setting the volume to 0, we need to - // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track - // whenever volume is being set might cause side effects, therefore we only handle - // "explicit mute operations", i.e., only after certain non-zero volume has been - // set, the subsequent volume setting operations will be consider as mute/un-mute - // operations and thus enable/disable the audio track. - if (mIsMuted && volume > 0) { - mIsMuted = false; - if (mEnabled) { - setStatus(true); - } - } else if (!mIsMuted && volume == 0) { - mIsMuted = true; - if (mEnabled) { - setStatus(false); - } - } - AUDIO_TRACK.setVolume(volume); - break; - case MSG_SET_AUDIO_TRACK: - mEnabled = (Integer) message == 1; - setStatus(mEnabled); - break; - case MSG_SET_PLAYBACK_SPEED: - mAudioClock.setPlaybackSpeed((Float) message); - break; - default: - super.handleMessage(messageType, message); - } - } - - private void setStatus(boolean enabled) { - if (enabled == AUDIO_TRACK.isEnabled()) { - return; - } - if (!enabled) { - // mAudioClock can be different from getPositionUs. In order to sync them, - // we set mAudioClock. - mAudioClock.setPositionUs(getPositionUs()); - } - AUDIO_TRACK.setStatus(enabled); - if (enabled) { - // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to - // the current position. If not, AUDIO_TRACK has the obsolete data. - seekTo(mAudioClock.getPositionUs()); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java deleted file mode 100644 index 142aa9b2..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java +++ /dev/null @@ -1,94 +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.exoplayer.audio; - -import android.os.Handler; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.SampleSource; - -/** - * MPEG-2 TS audio track renderer. - * - * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the - * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes - * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the - * presentation times to avoid the asynchronous Audio/Video problem. - */ -public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRenderer { - private final Ac3EventListener mListener; - - public interface Ac3EventListener extends EventListener { - /** - * Invoked when a {@link android.media.PlaybackParams} set to an - * {@link android.media.AudioTrack} is not valid. - * - * @param e The corresponding exception. - */ - void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); - } - - public MpegTsMediaCodecAudioTrackRenderer( - SampleSource source, - MediaCodecSelector mediaCodecSelector, - Handler eventHandler, - EventListener eventListener) { - super(source, mediaCodecSelector, eventHandler, eventListener); - mListener = (Ac3EventListener) eventListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_PLAYBACK_PARAMS) { - try { - super.handleMessage(messageType, message); - } catch (IllegalArgumentException e) { - if (isAudioTrackSetPlaybackParamsError(e)) { - notifyAudioTrackSetPlaybackParamsError(e); - } - } - return; - } - super.handleMessage(messageType, message); - } - - private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { - if (eventHandler != null && mListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - mListener.onAudioTrackSetPlaybackParamsError(e); - } - }); - } - } - - static private boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (e.getStackTrace() == null || e.getStackTrace().length < 1) { - return false; - } - for (StackTraceElement element : e.getStackTrace()) { - String elementString = element.toString(); - if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) { - return true; - } - } - return false; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java deleted file mode 100644 index 112e9dc4..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ /dev/null @@ -1,692 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.media.MediaFormat; -import android.os.ConditionVariable; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.util.ArrayMap; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.SampleHolder; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.exoplayer.SampleExtractor; -import com.android.tv.util.Utils; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -/** - * Manages {@link SampleChunk} objects. - * <p> - * The buffer manager can be disabled, while running, if the write throughput to the associated - * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}". - * This leads to restarting playback flow. - */ -public class BufferManager { - private static final String TAG = "BufferManager"; - private static final boolean DEBUG = false; - - // Constants for the disk write speed checking - private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK = - 10L * 1024 * 1024; // Checks for every 10M disk write - private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024; - private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times - private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second - - private final SampleChunk.SampleChunkCreator mSampleChunkCreator; - // Maps from track name to a map which maps from starting position to {@link SampleChunk}. - private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap = - new ArrayMap<>(); - private final Map<String, Long> mStartPositionMap = new ArrayMap<>(); - private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>(); - private final StorageManager mStorageManager; - private long mBufferSize = 0; - private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap(); - private final SampleChunk.ChunkCallback mChunkCallback = new SampleChunk.ChunkCallback() { - @Override - public void onChunkWrite(SampleChunk chunk) { - mBufferSize += chunk.getSize(); - } - - @Override - public void onChunkDelete(SampleChunk chunk) { - mBufferSize -= chunk.getSize(); - } - }; - - private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; - private long mTotalWriteSize; - private long mTotalWriteTimeNs; - private float mWriteBandwidth = 0.0f; - private volatile int mSpeedCheckCount; - - public interface ChunkEvictedListener { - void onChunkEvicted(String id, long createdTimeMs); - } - /** - * Handles I/O - * between BufferManager and {@link SampleExtractor}. - */ - public interface SampleBuffer { - - /** - * Initializes SampleBuffer. - * @param Ids track identifiers for storage read/write. - * @param mediaFormats meta-data for each track. - * @throws IOException - */ - void init(@NonNull List<String> Ids, - @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats) - throws IOException; - - /** - * Selects the track {@code index} for reading sample data. - */ - void selectTrack(int index); - - /** - * Deselects the track at {@code index}, - * so that no more samples will be read from the track. - */ - void deselectTrack(int index); - - /** - * Writes sample to storage. - * - * @param index track index - * @param sample sample to write at storage - * @param conditionVariable notifies the completion of writing sample. - * @throws IOException - */ - void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException; - - /** - * Checks whether storage write speed is slow. - */ - boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs); - - /** - * Handles when write speed is slow. - * @throws IOException - */ - void handleWriteSpeedSlow() throws IOException; - - /** - * Sets the flag when EoS was reached. - */ - void setEos(); - - /** - * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, - * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ} - * if it is available. - * If the next sample is not available, - * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}. - */ - int readSample(int index, SampleHolder outSample); - - /** - * Seeks to the specified time in microseconds. - */ - void seekTo(long positionUs); - - /** - * Returns an estimate of the position up to which data is buffered. - */ - long getBufferedPositionUs(); - - /** - * Returns whether there is buffered data. - */ - boolean continueBuffering(long positionUs); - - /** - * Cleans up and releases everything. - * @throws IOException - */ - void release() throws IOException; - } - - /** - * A Track format which will be loaded and saved from the permanent storage for recordings. - */ - public static class TrackFormat { - - /** - * The track id for the specified track. The track id will be used as a track identifier - * for recordings. - */ - public final String trackId; - - /** - * The {@link MediaFormat} for the specified track. - */ - public final MediaFormat format; - - /** - * Creates TrackFormat. - * @param trackId - * @param format - */ - public TrackFormat(String trackId, MediaFormat format) { - this.trackId = trackId; - this.format = format; - } - } - - /** - * A Holder for a sample position which will be loaded from the index file for recordings. - */ - public static class PositionHolder { - - /** - * The current sample position in microseconds. - * The position is identical to the PTS(presentation time stamp) of the sample. - */ - public final long positionUs; - - /** - * Base sample position for the current {@link SampleChunk}. - */ - public final long basePositionUs; - - /** - * The file offset for the current sample in the current {@link SampleChunk}. - */ - public final int offset; - - /** - * Creates a holder for a specific position in the recording. - * @param positionUs - * @param offset - */ - public PositionHolder(long positionUs, long basePositionUs, int offset) { - this.positionUs = positionUs; - this.basePositionUs = basePositionUs; - this.offset = offset; - } - } - - /** - * Storage configuration and policy manager for {@link BufferManager} - */ - public interface StorageManager { - - /** - * Provides eligible storage directory for {@link BufferManager}. - * - * @return a directory to save buffer(chunks) and meta files - */ - File getBufferDir(); - - /** - * Informs whether the storage is used for persistent use. (eg. dvr recording/play) - * - * @return {@code true} if stored files are persistent - */ - boolean isPersistent(); - - /** - * Informs whether the storage usage exceeds pre-determined size. - * - * @param bufferSize the current total usage of Storage in bytes. - * @param pendingDelete the current storage usage which will be deleted in near future by - * bytes - * @return {@code true} if it reached pre-determined max size - */ - boolean reachedStorageMax(long bufferSize, long pendingDelete); - - /** - * Informs whether the storage has enough remained space. - * - * @param pendingDelete the current storage usage which will be deleted in near future by - * bytes - * @return {@code true} if it has enough space - */ - boolean hasEnoughBuffer(long pendingDelete); - - /** - * Reads track name & {@link MediaFormat} from storage. - * - * @param isAudio {@code true} if it is for audio track - * @return {@link List} of TrackFormat - */ - List<TrackFormat> readTrackInfoFiles(boolean isAudio); - - /** - * Reads key sample positions for each written sample from storage. - * - * @param trackId track name - * @return indexes of the specified track - * @throws IOException - */ - ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException; - - /** - * Writes track information to storage. - * - * @param formatList {@list List} of TrackFormat - * @param isAudio {@code true} if it is for audio track - * @throws IOException - */ - void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) - throws IOException; - - /** - * Writes index file to storage. - * - * @param trackName track name - * @param index {@link SampleChunk} container - * @throws IOException - */ - void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) - throws IOException; - } - - private static class EvictChunkQueueMap { - private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>(); - private long mSize; - - private void init(String key) { - mEvictMap.put(key, new LinkedList<>()); - } - - private void add(String key, SampleChunk chunk) { - LinkedList<SampleChunk> queue = mEvictMap.get(key); - if (queue != null) { - mSize += chunk.getSize(); - queue.add(chunk); - } - } - - private SampleChunk poll(String key, long startPositionUs) { - LinkedList<SampleChunk> queue = mEvictMap.get(key); - if (queue != null) { - SampleChunk chunk = queue.peek(); - if (chunk != null && chunk.getStartPositionUs() < startPositionUs) { - mSize -= chunk.getSize(); - return queue.poll(); - } - } - return null; - } - - private long getSize() { - return mSize; - } - - private void release() { - for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) { - for (SampleChunk chunk : entry.getValue()) { - SampleChunk.IoState.release(chunk, true); - } - } - mEvictMap.clear(); - mSize = 0; - } - } - - public BufferManager(StorageManager storageManager) { - this(storageManager, new SampleChunk.SampleChunkCreator()); - } - - public BufferManager(StorageManager storageManager, - SampleChunk.SampleChunkCreator sampleChunkCreator) { - mStorageManager = storageManager; - mSampleChunkCreator = sampleChunkCreator; - } - - public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { - mEvictListeners.put(id, listener); - } - - public void unregisterChunkEvictedListener(String id) { - mEvictListeners.remove(id); - } - - private static String getFileName(String id, long positionUs) { - return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs); - } - - /** - * Creates a new {@link SampleChunk} for caching samples if it is needed. - * - * @param id the name of the track - * @param positionUs current position to write a sample in micro seconds. - * @param samplePool {@link SamplePool} for the fast creation of samples. - * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create - * a new {@link SampleChunk}. - * @param currentOffset the current offset to write. - * @return returns the created {@link SampleChunk}. - * @throws IOException - */ - public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, - SampleChunk currentChunk, int currentOffset) throws IOException { - if (!maybeEvictChunk()) { - throw new IOException("Not enough storage space"); - } - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); - if (map == null) { - map = new TreeMap<>(); - mChunkMap.put(id, map); - mStartPositionMap.put(id, positionUs); - mPendingDelete.init(id); - } - if (currentChunk == null) { - File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); - SampleChunk sampleChunk = mSampleChunkCreator - .createSampleChunk(samplePool, file, positionUs, mChunkCallback); - map.put(positionUs, new Pair(sampleChunk, 0)); - return sampleChunk; - } else { - map.put(positionUs, new Pair(currentChunk, currentOffset)); - return null; - } - } - - /** - * Loads a track using {@link BufferManager.StorageManager}. - * - * @param trackId the name of the track. - * @param samplePool {@link SamplePool} for the fast creation of samples. - * @throws IOException - */ - public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { - ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; - - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId); - if (map == null) { - map = new TreeMap<>(); - mChunkMap.put(trackId, map); - mStartPositionMap.put(trackId, startPositionUs); - mPendingDelete.init(trackId); - } - SampleChunk chunk = null; - long basePositionUs = -1; - for (PositionHolder position: keyPositions) { - if (position.basePositionUs != basePositionUs) { - chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, - mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs), - position.positionUs, mChunkCallback, chunk); - basePositionUs = position.basePositionUs; - } - map.put(position.positionUs, new Pair(chunk, position.offset)); - } - } - - /** - * Finds a {@link SampleChunk} for the specified track name and the position. - * - * @param id the name of the track. - * @param positionUs the position. - * @return returns the found {@link SampleChunk}. - */ - public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); - if (map == null) { - return null; - } - Pair<SampleChunk, Integer> ret; - SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1); - if (!headMap.isEmpty()) { - ret = headMap.get(headMap.lastKey()); - } else { - ret = map.get(map.firstKey()); - } - return ret; - } - - /** - * Evicts chunks which are ready to be evicted for the specified track - * - * @param id the specified track - * @param earlierThanPositionUs the start position of the {@link SampleChunk} - * should be earlier than - */ - public void evictChunks(String id, long earlierThanPositionUs) { - SampleChunk chunk = null; - while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) { - SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()) ; - } - } - - /** - * Returns the start position of the specified track in micro seconds. - * - * @param id the specified track - */ - public long getStartPositionUs(String id) { - Long ret = mStartPositionMap.get(id); - return ret == null ? 0 : ret; - } - - private boolean maybeEvictChunk() { - long pendingDelete = mPendingDelete.getSize(); - while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete) - || !mStorageManager.hasEnoughBuffer(pendingDelete)) { - if (mStorageManager.isPersistent()) { - // Since chunks are persistent, we cannot evict chunks. - return false; - } - SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null; - SampleChunk earliestChunk = null; - String earliestChunkId = null; - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); - if (map.isEmpty()) { - continue; - } - SampleChunk chunk = map.get(map.firstKey()).first; - if (earliestChunk == null - || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { - earliestChunkMap = map; - earliestChunk = chunk; - earliestChunkId = entry.getKey(); - } - } - if (earliestChunk == null) { - break; - } - mPendingDelete.add(earliestChunkId, earliestChunk); - earliestChunkMap.remove(earliestChunk.getStartPositionUs()); - if (DEBUG) { - Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; " - + "earliestChunk size = %d; %s@%d (%s)", - mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId, - earliestChunk.getStartPositionUs(), - Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs()))); - } - ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId); - if (listener != null) { - listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs()); - } - pendingDelete = mPendingDelete.getSize(); - } - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); - if (map.isEmpty()) { - continue; - } - mStartPositionMap.put(entry.getKey(), map.firstKey()); - } - return true; - } - - /** - * Reads track information which includes {@link MediaFormat}. - * - * @return returns all track information which is found by {@link BufferManager.StorageManager}. - * @throws IOException - */ - public List<TrackFormat> readTrackInfoFiles() throws IOException { - List<TrackFormat> trackFormatList = new ArrayList<>(); - trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false)); - trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true)); - if (trackFormatList.isEmpty()) { - throw new IOException("No track information to load"); - } - return trackFormatList; - } - - /** - * Writes track information and index information for all tracks. - * - * @param audios list of audio track information - * @param videos list of audio track information - * @throws IOException - */ - public void writeMetaFiles(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); - for (TrackFormat trackFormat : audios) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = - mChunkMap.get(trackFormat.trackId); - if (map == null) { - throw new IOException("Audio track index missing"); - } - mStorageManager.writeIndexFile(trackFormat.trackId, map); - } - } - if (!videos.isEmpty()) { - mStorageManager.writeTrackInfoFiles(videos, false); - for (TrackFormat trackFormat : videos) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = - mChunkMap.get(trackFormat.trackId); - if (map == null) { - throw new IOException("Video track index missing"); - } - mStorageManager.writeIndexFile(trackFormat.trackId, map); - } - } - } - - /** - * Releases all the resources. - */ - public void release() { - try { - mPendingDelete.release(); - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SampleChunk toRelease = null; - for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) { - if (toRelease != positions.first) { - toRelease = positions.first; - SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent()); - } - } - } - mChunkMap.clear(); - } catch (ConcurrentModificationException | NullPointerException e) { - // TODO: remove this after it it confirmed that race condition issues are resolved. - // b/32492258, b/32373376 - SoftPreconditions.checkState(false, "Exception on BufferManager#release: ", - e.toString()); - } - } - - private void resetWriteStat(float writeBandwidth) { - mWriteBandwidth = writeBandwidth; - mTotalWriteSize = 0; - mTotalWriteTimeNs = 0; - } - - /** - * Adds a disk write sample size to calculate the average disk write bandwidth. - */ - public void addWriteStat(long size, long timeNs) { - if (size >= mMinSampleSizeForSpeedCheck) { - mTotalWriteSize += size; - mTotalWriteTimeNs += timeNs; - } - } - - /** - * Returns if the average disk write bandwidth is slower than - * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}. - */ - public boolean isWriteSlow() { - if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) { - return false; - } - - // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers - // by temporary system overloading during the playback. - if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) { - return false; - } - mSpeedCheckCount++; - float megabytePerSecond = calculateWriteBandwidth(); - resetWriteStat(megabytePerSecond); - if (DEBUG) { - Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps"); - } - return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS; - } - - /** - * Returns recent write bandwidth in MBps. If recent bandwidth is not available, - * returns {float -1.0f}. - */ - public float getWriteBandwidth() { - return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth; - } - - private float calculateWriteBandwidth() { - if (mTotalWriteTimeNs == 0) { - return -1; - } - return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); - } - - /** - * Returns if {@link BufferManager} has checked the write speed, - * which is suitable for Trickplay. - */ - @VisibleForTesting - public boolean hasSpeedCheckDone() { - return mSpeedCheckCount > 0; - } - - /** - * Sets minimum sample size for write speed check. - * @param sampleSize minimum sample size for write speed check. - */ - @VisibleForTesting - public void setMinimumSampleSizeForSpeedCheck(int sampleSize) { - mMinSampleSizeForSpeedCheck = sampleSize; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java deleted file mode 100644 index 6a09016c..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.media.MediaFormat; -import android.util.Log; -import android.util.Pair; - -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.google.protobuf.nano.MessageNano; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; - -/** - * Manages DVR storage. - */ -public class DvrStorageManager implements BufferManager.StorageManager { - private static final String TAG = "DvrStorageManager"; - - // TODO: make serializable classes and use protobuf after internal data structure is finalized. - private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = - "com.google.android.videos.pixelWidthHeightRatio"; - private static final String META_FILE_TYPE_AUDIO = "audio"; - private static final String META_FILE_TYPE_VIDEO = "video"; - private static final String META_FILE_TYPE_CAPTION = "caption"; - private static final String META_FILE_SUFFIX = ".meta"; - private static final String IDX_FILE_SUFFIX = ".idx"; - private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2"; - - // Size of minimum reserved storage buffer which will be used to save meta files - // and index files after actual recording finished. - private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024; - private static final int NO_VALUE = -1; - private static final long NO_VALUE_LONG = -1L; - - private final File mBufferDir; - - // {@code true} when this is for recording, {@code false} when this is for replaying. - private final boolean mIsRecording; - - public DvrStorageManager(File file, boolean isRecording) { - mBufferDir = file; - mBufferDir.mkdirs(); - mIsRecording = isRecording; - } - - @Override - public File getBufferDir() { - return mBufferDir; - } - - @Override - public boolean isPersistent() { - return true; - } - - @Override - public boolean reachedStorageMax(long bufferSize, long pendingDelete) { - return false; - } - - @Override - public boolean hasEnoughBuffer(long pendingDelete) { - return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES; - } - - private void readFormatInt(DataInputStream in, MediaFormat format, String key) - throws IOException { - int val = in.readInt(); - if (val != NO_VALUE) { - format.setInteger(key, val); - } - } - - private void readFormatLong(DataInputStream in, MediaFormat format, String key) - throws IOException { - long val = in.readLong(); - if (val != NO_VALUE_LONG) { - format.setLong(key, val); - } - } - - private void readFormatFloat(DataInputStream in, MediaFormat format, String key) - throws IOException { - float val = in.readFloat(); - if (val != NO_VALUE) { - format.setFloat(key, val); - } - } - - private String readString(DataInputStream in) throws IOException { - int len = in.readInt(); - if (len <= 0) { - return null; - } - byte [] strBytes = new byte[len]; - in.readFully(strBytes); - return new String(strBytes, StandardCharsets.UTF_8); - } - - private void readFormatString(DataInputStream in, MediaFormat format, String key) - throws IOException { - String str = readString(in); - if (str != null) { - format.setString(key, str); - } - } - - private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) { - try { - String str = readString(in); - if (str != null) { - format.setString(key, str); - } - } catch (IOException e) { - // Since we are reading optional field, ignore the exception. - } - } - - private ByteBuffer readByteBuffer(DataInputStream in) throws IOException { - int len = in.readInt(); - if (len <= 0) { - return null; - } - byte [] bytes = new byte[len]; - in.readFully(bytes); - ByteBuffer buffer = ByteBuffer.allocate(len); - buffer.put(bytes); - buffer.flip(); - - return buffer; - } - - private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key) - throws IOException { - ByteBuffer buffer = readByteBuffer(in); - if (buffer != null) { - format.setByteBuffer(key, buffer); - } - } - - @Override - public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { - List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>(); - int index = 0; - boolean trackNotFound = false; - do { - String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) - + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - String name = readString(in); - MediaFormat format = new MediaFormat(); - readFormatString(in, format, MediaFormat.KEY_MIME); - readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); - readFormatInt(in, format, MediaFormat.KEY_WIDTH); - readFormatInt(in, format, MediaFormat.KEY_HEIGHT); - readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); - readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); - readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - readFormatByteBuffer(in, format, "csd-" + i); - } - readFormatLong(in, format, MediaFormat.KEY_DURATION); - - // This is optional since language field is added later. - readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE); - trackFormatList.add(new BufferManager.TrackFormat(name, format)); - } catch (IOException e) { - trackNotFound = true; - } - index++; - } while(!trackNotFound); - return trackFormatList; - } - - /** - * Reads caption information from files. - * - * @return a list of {@link AtscCaptionTrack} objects which store caption information. - */ - public List<AtscCaptionTrack> readCaptionInfoFiles() { - List<AtscCaptionTrack> tracks = new ArrayList<>(); - int index = 0; - boolean trackNotFound = false; - do { - String fileName = META_FILE_TYPE_CAPTION + - ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - byte[] data = new byte[(int) file.length()]; - in.read(data); - tracks.add(AtscCaptionTrack.parseFrom(data)); - } catch (IOException e) { - trackNotFound = true; - } - index++; - } while(!trackNotFound); - return tracks; - } - - private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile) - throws IOException { - ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); - try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { - long count = in.readLong(); - for (long i = 0; i < count; ++i) { - long positionUs = in.readLong(); - indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0)); - } - return indices; - } - } - - private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile) - throws IOException { - ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); - try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { - long count = in.readLong(); - for (long i = 0; i < count; ++i) { - long positionUs = in.readLong(); - long basePositionUs = in.readLong(); - int offset = in.readInt(); - indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset)); - } - return indices; - } - } - - @Override - public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) - throws IOException { - File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2); - if (file.exists()) { - return readNewIndexFile(file); - } else { - return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX)); - } - } - - private void writeFormatInt(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - out.writeInt(format.getInteger(key)); - } else { - out.writeInt(NO_VALUE); - } - } - - private void writeFormatLong(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - out.writeLong(format.getLong(key)); - } else { - out.writeLong(NO_VALUE_LONG); - } - } - - private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - out.writeFloat(format.getFloat(key)); - } else { - out.writeFloat(NO_VALUE); - } - } - - private void writeString(DataOutputStream out, String str) throws IOException { - byte [] data = str.getBytes(StandardCharsets.UTF_8); - out.writeInt(data.length); - if (data.length > 0) { - out.write(data); - } - } - - private void writeFormatString(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - writeString(out, format.getString(key)); - } else { - out.writeInt(0); - } - } - - private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException { - byte [] data = new byte[buffer.limit()]; - buffer.get(data); - buffer.flip(); - out.writeInt(data.length); - if (data.length > 0) { - out.write(data); - } else { - out.writeInt(0); - } - } - - private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - writeByteBuffer(out, format.getByteBuffer(key)); - } else { - out.writeInt(0); - } - } - - @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) - throws IOException { - for (int i = 0; i < formatList.size() ; ++i) { - BufferManager.TrackFormat trackFormat = formatList.get(i); - String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) - + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - writeString(out, trackFormat.trackId); - writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT); - writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE); - writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int j = 0; j < 3; ++j) { - writeFormatByteBuffer(out, trackFormat.format, "csd-" + j); - } - writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION); - writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE); - } - } - } - - /** - * Writes caption information to files. - * - * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information. - */ - public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) { - if (tracks == null || tracks.isEmpty()) { - return; - } - for (int i = 0; i < tracks.size(); i++) { - AtscCaptionTrack track = tracks.get(i); - String fileName = META_FILE_TYPE_CAPTION + - ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); - File file = new File(getBufferDir(), fileName); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - out.write(MessageNano.toByteArray(track)); - } catch (Exception e) { - Log.e(TAG, "Fail to write caption info to files", e); - } - } - } - - @Override - public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) - throws IOException { - File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { - out.writeLong(index.size()); - for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) { - out.writeLong(entry.getKey()); - out.writeLong(entry.getValue().first.getStartPositionUs()); - out.writeInt(entry.getValue().second); - } - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java deleted file mode 100644 index af0c3f0d..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ /dev/null @@ -1,309 +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.exoplayer.buffer; - -import android.os.ConditionVariable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.util.Log; - -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.tuner.exoplayer.MpegTsPlayer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; -import com.android.tv.tuner.exoplayer.SampleExtractor; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Handles I/O between {@link SampleExtractor} and - * {@link BufferManager}.Reads & writes samples from/to {@link SampleChunk} which is backed - * by physical storage. - */ -public class RecordingSampleBuffer implements BufferManager.SampleBuffer, - BufferManager.ChunkEvictedListener { - private static final String TAG = "RecordingSampleBuffer"; - - @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING}) - @Retention(RetentionPolicy.SOURCE) - public @interface BufferReason {} - - /** - * A buffer reason for live-stream playback. - */ - public static final int BUFFER_REASON_LIVE_PLAYBACK = 0; - - /** - * A buffer reason for playback of a recorded program. - */ - public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1; - - /** - * A buffer reason for recording a program. - */ - public static final int BUFFER_REASON_RECORDING = 2; - - /** - * The minimum duration to support seek in Trickplay. - */ - static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); - - /** - * The duration of a {@link SampleChunk} for recordings. - */ - static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes - private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds - private static final long BUFFER_NEEDED_US = - 1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS); - - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - private final @BufferReason int mBufferReason; - - private int mTrackCount; - private boolean[] mTrackSelected; - private List<SampleQueue> mReadSampleQueues; - private final SamplePool mSamplePool = new SamplePool(); - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - private long mCurrentPlaybackPositionUs = 0; - - // An error in I/O thread of {@link SampleChunkIoHelper} will be notified. - private volatile boolean mError; - - // Eos was reached in I/O thread of {@link SampleChunkIoHelper}. - private volatile boolean mEos; - private SampleChunkIoHelper mSampleChunkIoHelper; - private final SampleChunkIoHelper.IoCallback mIoCallback = - new SampleChunkIoHelper.IoCallback() { - @Override - public void onIoReachedEos() { - mEos = true; - } - - @Override - public void onIoError() { - mError = true; - } - }; - - /** - * Creates {@link BufferManager.SampleBuffer} with - * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). - * - * @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} - */ - public RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener, - boolean enableTrickplay, @BufferReason int bufferReason) { - mBufferManager = bufferManager; - mBufferListener = bufferListener; - if (bufferListener != null) { - bufferListener.onBufferStateChanged(enableTrickplay); - } - mBufferReason = bufferReason; - } - - @Override - public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats) - throws IOException { - mTrackCount = ids.size(); - if (mTrackCount <= 0) { - throw new IOException("No tracks to initialize"); - } - mTrackSelected = new boolean[mTrackCount]; - mReadSampleQueues = new ArrayList<>(); - mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, - mBufferManager, mSamplePool, mIoCallback); - for (int i = 0; i < mTrackCount; ++i) { - mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); - } - mSampleChunkIoHelper.init(); - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); - } - } - - @Override - public void selectTrack(int index) { - if (!mTrackSelected[index]) { - mTrackSelected[index] = true; - mReadSampleQueues.get(index).clear(); - mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); - } - } - - @Override - public void deselectTrack(int index) { - if (mTrackSelected[index]) { - mTrackSelected[index] = false; - mReadSampleQueues.get(index).clear(); - mSampleChunkIoHelper.closeRead(index); - } - } - - @Override - public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - mSampleChunkIoHelper.writeSample(index, sample, conditionVariable); - - if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) { - Log.e(TAG, "Error: Serious delay on writing buffer"); - conditionVariable.block(); - } - } - - @Override - public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { - if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) { - return false; - } - mBufferManager.addWriteStat(sampleSize, writeDurationNs); - return mBufferManager.isWriteSlow(); - } - - @Override - public void handleWriteSpeedSlow() throws IOException{ - if (mBufferReason == BUFFER_REASON_RECORDING) { - // Recording does not need to stop because I/O speed is slow temporarily. - // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS. - // Reaching EoS will stop recording eventually. - Log.w(TAG, "Disk I/O speed is slow for recording temporarily: " - + mBufferManager.getWriteBandwidth() + "MBps"); - return; - } - // Disables buffering samples afterwards, and notifies the disk speed is slow. - Log.w(TAG, "Disk is too slow for trickplay"); - mBufferListener.onDiskTooSlow(); - } - - @Override - public void setEos() { - mSampleChunkIoHelper.closeWrite(); - } - - private boolean maybeReadSample(SampleQueue queue, int index) { - if (queue.getLastQueuedPositionUs() != null - && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US - && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) { - // The speed of queuing samples can be higher than the playback speed. - // If the duration of the samples in the queue is not limited, - // samples can be accumulated and there can be out-of-memory issues. - // But, the throttling should provide enough samples for the player to - // finish the buffering state. - return false; - } - SampleHolder sample = mSampleChunkIoHelper.readSample(index); - if (sample != null) { - queue.queueSample(sample); - return true; - } - return false; - } - - @Override - public int readSample(int track, SampleHolder outSample) { - Assertions.checkState(mTrackSelected[track]); - maybeReadSample(mReadSampleQueues.get(track), track); - int result = mReadSampleQueues.get(track).dequeueSample(outSample); - if ((result != SampleSource.SAMPLE_READ && mEos) || mError) { - return SampleSource.END_OF_STREAM; - } - return result; - } - - @Override - public void seekTo(long positionUs) { - for (int i = 0; i < mTrackCount; ++i) { - if (mTrackSelected[i]) { - mReadSampleQueues.get(i).clear(); - mSampleChunkIoHelper.openRead(i, positionUs); - } - } - mLastBufferedPositionUs = positionUs; - } - - @Override - public long getBufferedPositionUs() { - Long result = null; - for (int i = 0; i < mTrackCount; ++i) { - if (!mTrackSelected[i]) { - continue; - } - Long lastQueuedSamplePositionUs = - mReadSampleQueues.get(i).getLastQueuedPositionUs(); - if (lastQueuedSamplePositionUs == null) { - // No sample has been queued. - result = mLastBufferedPositionUs; - continue; - } - if (result == null || result > lastQueuedSamplePositionUs) { - result = lastQueuedSamplePositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } - return (mLastBufferedPositionUs = result); - } - - @Override - public boolean continueBuffering(long positionUs) { - mCurrentPlaybackPositionUs = positionUs; - for (int i = 0; i < mTrackCount; ++i) { - if (!mTrackSelected[i]) { - continue; - } - SampleQueue queue = mReadSampleQueues.get(i); - maybeReadSample(queue, i); - if (queue.getLastQueuedPositionUs() == null - || positionUs > queue.getLastQueuedPositionUs()) { - // No more buffered data. - return false; - } - } - return true; - } - - @Override - public void release() throws IOException { - if (mTrackCount <= 0) { - return; - } - if (mSampleChunkIoHelper != null) { - mSampleChunkIoHelper.release(); - } - } - - // onChunkEvictedListener - @Override - public void onChunkEvicted(String id, long createdTimeMs) { - if (mBufferListener != null) { - mBufferListener.onBufferStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US)); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java deleted file mode 100644 index 04b5a071..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java +++ /dev/null @@ -1,437 +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.exoplayer.buffer; - -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.google.android.exoplayer.SampleHolder; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; - -/** - * {@link SampleChunk} stores samples into file and makes them available for read. - * Stored file = { Header, Sample } * N - * Header = sample size : int, sample flag : int, sample PTS in micro second : long - */ -public class SampleChunk { - private static final String TAG = "SampleChunk"; - private static final boolean DEBUG = false; - - private final long mCreatedTimeMs; - private final long mStartPositionUs; - private SampleChunk mNextChunk; - - // Header = sample size : int, sample flag : int, sample PTS in micro second : long - private static final int SAMPLE_HEADER_LENGTH = 16; - - private final File mFile; - private final ChunkCallback mChunkCallback; - private final SamplePool mSamplePool; - private RandomAccessFile mAccessFile; - private long mWriteOffset; - private boolean mWriteFinished; - private boolean mIsReading; - private boolean mIsWriting; - - /** - * A callback for chunks being committed to permanent storage. - */ - public static abstract class ChunkCallback { - - /** - * Notifies when writing a SampleChunk is completed. - * - * @param chunk SampleChunk which is written completely - */ - public void onChunkWrite(SampleChunk chunk) { - - } - - /** - * Notifies when a SampleChunk is deleted. - * - * @param chunk SampleChunk which is deleted from storage - */ - public void onChunkDelete(SampleChunk chunk) { - } - } - - /** - * A class for SampleChunk creation. - */ - public static class SampleChunkCreator { - - /** - * Returns a newly created SampleChunk to read & write samples. - * - * @param samplePool sample allocator - * @param file filename which will be created newly - * @param startPositionUs the start position of the earliest sample to be stored - * @param chunkCallback for total storage usage change notification - */ - SampleChunk createSampleChunk(SamplePool samplePool, File file, - long startPositionUs, ChunkCallback chunkCallback) { - return new SampleChunk(samplePool, file, startPositionUs, System.currentTimeMillis(), - chunkCallback); - } - - /** - * Returns a newly created SampleChunk which is backed by an existing file. - * Created SampleChunk is read-only. - * - * @param samplePool sample allocator - * @param bufferDir the directory where the file to read is located - * @param filename the filename which will be read afterwards - * @param startPositionUs the start position of the earliest sample in the file - * @param chunkCallback for total storage usage change notification - * @param prev the previous SampleChunk just before the newly created SampleChunk - * @throws IOException - */ - SampleChunk loadSampleChunkFromFile(SamplePool samplePool, File bufferDir, - String filename, long startPositionUs, ChunkCallback chunkCallback, - SampleChunk prev) throws IOException { - File file = new File(bufferDir, filename); - SampleChunk chunk = - new SampleChunk(samplePool, file, startPositionUs, chunkCallback); - if (prev != null) { - prev.mNextChunk = chunk; - } - return chunk; - } - } - - /** - * Handles I/O for SampleChunk. - * Maintains current SampleChunk and the current offset for next I/O operation. - */ - static class IoState { - private SampleChunk mChunk; - private long mCurrentOffset; - - private boolean equals(SampleChunk chunk, long offset) { - return chunk == mChunk && mCurrentOffset == offset; - } - - /** - * Returns whether read I/O operation is finished. - */ - boolean isReadFinished() { - return mChunk == null; - } - - /** - * Returns the start position of the current SampleChunk - */ - long getStartPositionUs() { - return mChunk == null ? 0 : mChunk.getStartPositionUs(); - } - - private void reset(@Nullable SampleChunk chunk) { - mChunk = chunk; - mCurrentOffset = 0; - } - - private void reset(SampleChunk chunk, long offset) { - mChunk = chunk; - mCurrentOffset = offset; - } - - /** - * Prepares for read I/O operation from a new SampleChunk. - * - * @param chunk the new SampleChunk to read from - * @throws IOException - */ - void openRead(SampleChunk chunk, long offset) throws IOException { - if (mChunk != null) { - mChunk.closeRead(); - } - chunk.openRead(); - reset(chunk, offset); - } - - /** - * Prepares for write I/O operation to a new SampleChunk. - * - * @param chunk the new SampleChunk to write samples afterwards - * @throws IOException - */ - void openWrite(SampleChunk chunk) throws IOException{ - if (mChunk != null) { - mChunk.closeWrite(chunk); - } - chunk.openWrite(); - reset(chunk); - } - - /** - * Reads a sample if it is available. - * - * @return Returns a sample if it is available, null otherwise. - * @throws IOException - */ - SampleHolder read() throws IOException { - if (mChunk != null && mChunk.isReadFinished(this)) { - SampleChunk next = mChunk.mNextChunk; - mChunk.closeRead(); - if (next != null) { - next.openRead(); - } - reset(next); - } - if (mChunk != null) { - try { - return mChunk.read(this); - } catch (IllegalStateException e) { - // Write is finished and there is no additional buffer to read. - Log.w(TAG, "Tried to read sample over EOS."); - return null; - } - } else { - return null; - } - } - - /** - * Writes a sample. - * - * @param sample to write - * @param nextChunk if this is {@code null} writes at the current SampleChunk, - * otherwise close current SampleChunk and writes at this - * @throws IOException - */ - void write(SampleHolder sample, SampleChunk nextChunk) - throws IOException { - if (nextChunk != null) { - if (mChunk == null || mChunk.mNextChunk != null) { - throw new IllegalStateException("Requested write for wrong SampleChunk"); - } - mChunk.closeWrite(nextChunk); - mChunk.mChunkCallback.onChunkWrite(mChunk); - nextChunk.openWrite(); - reset(nextChunk); - } - mChunk.write(sample, this); - } - - /** - * Finishes write I/O operation. - * - * @throws IOException - */ - void closeWrite() throws IOException { - if (mChunk != null) { - mChunk.closeWrite(null); - } - } - - /** - * Returns the current SampleChunk for subsequent I/O operation. - */ - SampleChunk getChunk() { - return mChunk; - } - - /** - * Returns the current offset of the current SampleChunk for subsequent I/O operation. - */ - long getOffset() { - return mCurrentOffset; - } - - /** - * Releases SampleChunk. the SampleChunk will not be used anymore. - * - * @param chunk to release - * @param delete {@code true} when the backed file needs to be deleted, - * {@code false} otherwise. - */ - static void release(SampleChunk chunk, boolean delete) { - chunk.release(delete); - } - } - - @VisibleForTesting - protected SampleChunk(SamplePool samplePool, File file, long startPositionUs, - long createdTimeMs, ChunkCallback chunkCallback) { - mStartPositionUs = startPositionUs; - mCreatedTimeMs = createdTimeMs; - mSamplePool = samplePool; - mFile = file; - mChunkCallback = chunkCallback; - } - - // Constructor of SampleChunk which is backed by the given existing file. - private SampleChunk(SamplePool samplePool, File file, long startPositionUs, - ChunkCallback chunkCallback) throws IOException { - mStartPositionUs = startPositionUs; - mCreatedTimeMs = mStartPositionUs / 1000; - mSamplePool = samplePool; - mFile = file; - mChunkCallback = chunkCallback; - mWriteFinished = true; - } - - private void openRead() throws IOException { - if (!mIsReading) { - if (mAccessFile == null) { - mAccessFile = new RandomAccessFile(mFile, "r"); - } - if (mWriteFinished && mWriteOffset == 0) { - // Lazy loading of write offset, in order not to load - // all SampleChunk's write offset at start time of recorded playback. - mWriteOffset = mAccessFile.length(); - } - mIsReading = true; - } - } - - private void openWrite() throws IOException { - if (mWriteFinished) { - throw new IllegalStateException("Opened for write though write is already finished"); - } - if (!mIsWriting) { - if (mIsReading) { - throw new IllegalStateException("Write is requested for " - + "an already opened SampleChunk"); - } - mAccessFile = new RandomAccessFile(mFile, "rw"); - mIsWriting = true; - } - } - - private void CloseAccessFileIfNeeded() throws IOException { - if (!mIsReading && !mIsWriting) { - try { - if (mAccessFile != null) { - mAccessFile.close(); - } - } finally { - mAccessFile = null; - } - } - } - - private void closeRead() throws IOException{ - if (mIsReading) { - mIsReading = false; - CloseAccessFileIfNeeded(); - } - } - - private void closeWrite(SampleChunk nextChunk) - throws IOException { - if (mIsWriting) { - mNextChunk = nextChunk; - mIsWriting = false; - mWriteFinished = true; - CloseAccessFileIfNeeded(); - } - } - - private boolean isReadFinished(IoState state) { - return mWriteFinished && state.equals(this, mWriteOffset); - } - - private SampleHolder read(IoState state) throws IOException { - if (mAccessFile == null || state.mChunk != this) { - throw new IllegalStateException("Requested read for wrong SampleChunk"); - } - long offset = state.mCurrentOffset; - if (offset >= mWriteOffset) { - if (mWriteFinished) { - throw new IllegalStateException("Requested read for wrong range"); - } else { - if (offset != mWriteOffset) { - Log.e(TAG, "This should not happen!"); - } - return null; - } - } - mAccessFile.seek(offset); - int size = mAccessFile.readInt(); - SampleHolder sample = mSamplePool.acquireSample(size); - sample.size = size; - sample.flags = mAccessFile.readInt(); - sample.timeUs = mAccessFile.readLong(); - sample.clearData(); - sample.data.put(mAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, - offset + SAMPLE_HEADER_LENGTH, sample.size)); - offset += sample.size + SAMPLE_HEADER_LENGTH; - state.mCurrentOffset = offset; - return sample; - } - - @VisibleForTesting - protected void write(SampleHolder sample, IoState state) - throws IOException { - if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) { - throw new IllegalStateException("Requested write for wrong SampleChunk"); - } - - mAccessFile.seek(mWriteOffset); - mAccessFile.writeInt(sample.size); - mAccessFile.writeInt(sample.flags); - mAccessFile.writeLong(sample.timeUs); - sample.data.position(0).limit(sample.size); - mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data); - mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH; - state.mCurrentOffset = mWriteOffset; - } - - private void release(boolean delete) { - mWriteFinished = true; - mIsReading = mIsWriting = false; - try { - if (mAccessFile != null) { - mAccessFile.close(); - } - } catch (IOException e) { - // Since the SampleChunk will not be reused, ignore exception. - } - if (delete) { - mFile.delete(); - mChunkCallback.onChunkDelete(this); - } - } - - /** - * Returns the start position. - */ - public long getStartPositionUs() { - return mStartPositionUs; - } - - /** - * Returns the creation time. - */ - public long getCreatedTimeMs() { - return mCreatedTimeMs; - } - - /** - * Returns the current size. - */ - public long getSize() { - return mWriteOffset; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java deleted file mode 100644 index ca97a91a..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ /dev/null @@ -1,461 +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.exoplayer.buffer; - -import android.media.MediaCodec; -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.util.ArraySet; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.util.MimeTypes; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * Handles all {@link SampleChunk} I/O operations. - * An I/O dedicated thread handles all I/O operations for synchronization. - */ -public class SampleChunkIoHelper implements Handler.Callback { - private static final String TAG = "SampleChunkIoHelper"; - - private static final int MAX_READ_BUFFER_SAMPLES = 3; - private static final int READ_RESCHEDULING_DELAY_MS = 10; - - private static final int MSG_OPEN_READ = 1; - private static final int MSG_OPEN_WRITE = 2; - private static final int MSG_CLOSE_READ = 3; - private static final int MSG_CLOSE_WRITE = 4; - private static final int MSG_READ = 5; - private static final int MSG_WRITE = 6; - private static final int MSG_RELEASE = 7; - - private final long mSampleChunkDurationUs; - private final int mTrackCount; - private final List<String> mIds; - private final List<MediaFormat> mMediaFormats; - private final @BufferReason int mBufferReason; - private final BufferManager mBufferManager; - private final SamplePool mSamplePool; - private final IoCallback mIoCallback; - - private Handler mIoHandler; - private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[]; - private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[]; - private final long[] mWriteIndexEndPositionUs; - private final long[] mWriteChunkEndPositionUs; - private final SampleChunk.IoState[] mReadIoStates; - private final SampleChunk.IoState[] mWriteIoStates; - private final Set<Integer> mSelectedTracks = new ArraySet<>(); - private long mBufferDurationUs = 0; - private boolean mWriteEnded; - private boolean mErrorNotified; - private boolean mFinished; - - /** - * A Callback for I/O events. - */ - public static abstract class IoCallback { - - /** - * Called when there is no sample to read. - */ - public void onIoReachedEos() { - } - - /** - * Called when there is an irrecoverable error during I/O. - */ - public void onIoError() { - } - } - - private class IoParams { - private final int index; - private final long positionUs; - private final SampleHolder sample; - private final ConditionVariable conditionVariable; - private final ConcurrentLinkedQueue<SampleHolder> readSampleBuffer; - - private IoParams(int index, long positionUs, SampleHolder sample, - ConditionVariable conditionVariable, - ConcurrentLinkedQueue<SampleHolder> readSampleBuffer) { - this.index = index; - this.positionUs = positionUs; - this.sample = sample; - this.conditionVariable = conditionVariable; - this.readSampleBuffer = readSampleBuffer; - } - } - - /** - * Creates {@link SampleChunk} I/O handler. - * - * @param ids track names - * @param mediaFormats {@link android.media.MediaFormat} for each track - * @param bufferReason reason to be buffered - * @param bufferManager manager of {@link SampleChunk} collections - * @param samplePool allocator for a sample - * @param ioCallback listeners for I/O events - */ - public SampleChunkIoHelper(List<String> ids, List<MediaFormat> mediaFormats, - @BufferReason int bufferReason, BufferManager bufferManager, SamplePool samplePool, - IoCallback ioCallback) { - mTrackCount = ids.size(); - mIds = ids; - mMediaFormats = mediaFormats; - mBufferReason = bufferReason; - mBufferManager = bufferManager; - mSamplePool = samplePool; - mIoCallback = ioCallback; - - mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mWriteIndexEndPositionUs = new long[mTrackCount]; - mWriteChunkEndPositionUs = new long[mTrackCount]; - mReadIoStates = new SampleChunk.IoState[mTrackCount]; - mWriteIoStates = new SampleChunk.IoState[mTrackCount]; - - // Small chunk duration for live playback will give more fine grained storage usage - // and eviction handling for trickplay. - mSampleChunkDurationUs = - bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ? - RecordingSampleBuffer.MIN_SEEK_DURATION_US : - RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US; - for (int i = 0; i < mTrackCount; ++i) { - mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US; - mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs; - mReadIoStates[i] = new SampleChunk.IoState(); - mWriteIoStates[i] = new SampleChunk.IoState(); - } - } - - /** - * Prepares and initializes for I/O operations. - * - * @throws IOException - */ - public void init() throws IOException { - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mIoHandler = new Handler(handlerThread.getLooper(), this); - if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) { - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.loadTrackFromStorage(mIds.get(i), mSamplePool); - } - mWriteEnded = true; - } else { - for (int i = 0; i < mTrackCount; ++i) { - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i)); - } - } - } - - /** - * Reads a sample if it is available. - * - * @param index track index - * @return {@code null} if a sample is not available, otherwise returns a sample - */ - public SampleHolder readSample(int index) { - SampleHolder sample = mReadSampleBuffers[index].poll(); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index)); - return sample; - } - - /** - * Writes a sample. - * - * @param index track index - * @param sample to write - * @param conditionVariable which will be wait until the write is finished - * @throws IOException - */ - public void writeSample(int index, SampleHolder sample, - ConditionVariable conditionVariable) throws IOException { - if (mErrorNotified) { - throw new IOException("Storage I/O error happened"); - } - conditionVariable.close(); - IoParams params = new IoParams(index, 0, sample, conditionVariable, null); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_WRITE, params)); - } - - /** - * Starts read from the specified position. - * - * @param index track index - * @param positionUs the specified position - */ - public void openRead(int index, long positionUs) { - // Old mReadSampleBuffers may have a pending read. - mReadSampleBuffers[index] = new ConcurrentLinkedQueue<>(); - IoParams params = new IoParams(index, positionUs, null, null, mReadSampleBuffers[index]); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_READ, params)); - } - - /** - * Closes read from the specified track. - * - * @param index track index - */ - public void closeRead(int index) { - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index)); - } - - /** - * Notifies writes are finished. - */ - public void closeWrite() { - mIoHandler.sendEmptyMessage(MSG_CLOSE_WRITE); - } - - /** - * Finishes I/O operations and releases all the resources. - * @throws IOException - */ - public void release() throws IOException { - if (mIoHandler == null) { - return; - } - // Finishes all I/O operations. - ConditionVariable conditionVariable = new ConditionVariable(); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_RELEASE, conditionVariable)); - conditionVariable.block(); - - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.unregisterChunkEvictedListener(mIds.get(i)); - } - try { - if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) { - // Saves meta information for recording. - List<BufferManager.TrackFormat> audios = new LinkedList<>(); - List<BufferManager.TrackFormat> videos = new LinkedList<>(); - 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.writeMetaFiles(audios, videos); - } - } finally { - mBufferManager.release(); - mIoHandler.getLooper().quitSafely(); - } - } - - @Override - public boolean handleMessage(Message message) { - if (mFinished) { - return true; - } - releaseEvictedChunks(); - try { - switch (message.what) { - case MSG_OPEN_READ: - doOpenRead((IoParams) message.obj); - return true; - case MSG_OPEN_WRITE: - doOpenWrite((int) message.obj); - return true; - case MSG_CLOSE_READ: - doCloseRead((int) message.obj); - return true; - case MSG_CLOSE_WRITE: - doCloseWrite(); - return true; - case MSG_READ: - doRead((int) message.obj); - return true; - case MSG_WRITE: - doWrite((IoParams) message.obj); - // Since only write will increase storage, eviction will be handled here. - return true; - case MSG_RELEASE: - doRelease((ConditionVariable) message.obj); - return true; - } - } catch (IOException e) { - mIoCallback.onIoError(); - mErrorNotified = true; - Log.e(TAG, "IoException happened", e); - return true; - } - return false; - } - - private void doOpenRead(IoParams params) throws IOException { - int index = params.index; - mIoHandler.removeMessages(MSG_READ, index); - Pair<SampleChunk, Integer> readPosition = - mBufferManager.getReadFile(mIds.get(index), params.positionUs); - if (readPosition == null) { - String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs - + "is not found"; - SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage); - throw new IOException(errorMessage); - } - mSelectedTracks.add(index); - mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second); - if (mHandlerReadSampleBuffers[index] != null) { - SampleHolder sample; - while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { - mSamplePool.releaseSample(sample); - } - } - mHandlerReadSampleBuffers[index] = params.readSampleBuffer; - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index)); - } - - private void doOpenWrite(int index) throws IOException { - SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, - mSamplePool, null, 0); - mWriteIoStates[index].openWrite(chunk); - } - - private void doCloseRead(int index) { - mSelectedTracks.remove(index); - if (mHandlerReadSampleBuffers[index] != null) { - SampleHolder sample; - while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { - mSamplePool.releaseSample(sample); - } - } - mIoHandler.removeMessages(MSG_READ, index); - } - - private void doRead(int index) throws IOException { - mIoHandler.removeMessages(MSG_READ, index); - if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) { - // If enough samples are buffered, try again few moments later hoping that - // buffered samples are consumed. - mIoHandler.sendMessageDelayed( - mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS); - } else { - if (mReadIoStates[index].isReadFinished()) { - for (int i = 0; i < mTrackCount; ++i) { - if (!mReadIoStates[i].isReadFinished()) { - return; - } - } - mIoCallback.onIoReachedEos(); - return; - } - SampleHolder sample = mReadIoStates[index].read(); - if (sample != null) { - mHandlerReadSampleBuffers[index].offer(sample); - } else { - // Read reached write but write is not finished yet --- wait a few moments to - // see if another sample is written. - mIoHandler.sendMessageDelayed( - mIoHandler.obtainMessage(MSG_READ, index), - READ_RESCHEDULING_DELAY_MS); - } - } - } - - private void doWrite(IoParams params) throws IOException { - try { - if (mWriteEnded) { - SoftPreconditions.checkState(false); - return; - } - int index = params.index; - SampleHolder sample = params.sample; - SampleChunk nextChunk = null; - if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { - if (sample.timeUs > mBufferDurationUs) { - mBufferDurationUs = sample.timeUs; - } - if (sample.timeUs >= mWriteIndexEndPositionUs[index]) { - SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ? - null : mWriteIoStates[params.index].getChunk(); - int currentOffset = (int) mWriteIoStates[params.index].getOffset(); - nextChunk = mBufferManager.createNewWriteFileIfNeeded( - mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool, - currentChunk, currentOffset); - mWriteIndexEndPositionUs[index] = - ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) * - RecordingSampleBuffer.MIN_SEEK_DURATION_US; - if (nextChunk != null) { - mWriteChunkEndPositionUs[index] = - ((sample.timeUs / mSampleChunkDurationUs) + 1) - * mSampleChunkDurationUs; - } - } - } - mWriteIoStates[params.index].write(params.sample, nextChunk); - } finally { - params.conditionVariable.open(); - } - } - - private void doCloseWrite() throws IOException { - if (mWriteEnded) { - return; - } - mWriteEnded = true; - boolean readFinished = true; - for (int i = 0; i < mTrackCount; ++i) { - readFinished = readFinished && mReadIoStates[i].isReadFinished(); - mWriteIoStates[i].closeWrite(); - } - if (readFinished) { - mIoCallback.onIoReachedEos(); - } - } - - private void doRelease(ConditionVariable conditionVariable) { - mIoHandler.removeCallbacksAndMessages(null); - mFinished = true; - conditionVariable.open(); - mSelectedTracks.clear(); - } - - private void releaseEvictedChunks() { - if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK - || mSelectedTracks.isEmpty()) { - return; - } - long currentStartPositionUs = Long.MAX_VALUE; - for (int trackIndex : mSelectedTracks) { - currentStartPositionUs = Math.min(currentStartPositionUs, - mReadIoStates[trackIndex].getStartPositionUs()); - } - for (int i = 0; i < mTrackCount; ++i) { - long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)), - currentStartPositionUs); - mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java b/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java deleted file mode 100644 index bb048e85..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import com.google.android.exoplayer.SampleHolder; - -import java.util.LinkedList; - -/** - * Pool of samples to recycle ByteBuffers as much as possible. - */ -public class SamplePool { - private final LinkedList<SampleHolder> mSamplePool = new LinkedList<>(); - - /** - * Acquires a sample with a buffer larger than size from the pool. Allocate new one or resize - * an existing buffer if necessary. - */ - public synchronized SampleHolder acquireSample(int size) { - if (mSamplePool.isEmpty()) { - SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.ensureSpaceForWrite(size); - return sample; - } - SampleHolder smallestSufficientSample = null; - SampleHolder maxSample = mSamplePool.getFirst(); - for (SampleHolder sample : mSamplePool) { - // Grab the smallest sufficient sample. - if (sample.data.capacity() >= size && (smallestSufficientSample == null - || smallestSufficientSample.data.capacity() > sample.data.capacity())) { - smallestSufficientSample = sample; - } - - // Grab the max size sample. - if (maxSample.data.capacity() < sample.data.capacity()) { - maxSample = sample; - } - } - SampleHolder sampleFromPool = smallestSufficientSample; - - // If there's no sufficient sample, grab the maximum sample and resize it to size. - if (sampleFromPool == null) { - sampleFromPool = maxSample; - sampleFromPool.ensureSpaceForWrite(size); - } - mSamplePool.remove(sampleFromPool); - return sampleFromPool; - } - - /** - * Releases the sample back to the pool. - */ - public synchronized void releaseSample(SampleHolder sample) { - sample.clearData(); - mSamplePool.offerLast(sample); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java deleted file mode 100644 index 75eac5a2..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; - -import java.util.LinkedList; - -/** - * A sample queue which reads from the buffer and passes to player pipeline. - */ -public class SampleQueue { - private final LinkedList<SampleHolder> mQueue = new LinkedList<>(); - private final SamplePool mSamplePool; - private Long mLastQueuedPositionUs = null; - - public SampleQueue(SamplePool samplePool) { - mSamplePool = samplePool; - } - - public void queueSample(SampleHolder sample) { - mQueue.offer(sample); - mLastQueuedPositionUs = sample.timeUs; - } - - public int dequeueSample(SampleHolder sample) { - SampleHolder sampleFromQueue = mQueue.poll(); - if (sampleFromQueue == null) { - return SampleSource.NOTHING_READ; - } - sample.ensureSpaceForWrite(sampleFromQueue.size); - sample.size = sampleFromQueue.size; - sample.flags = sampleFromQueue.flags; - sample.timeUs = sampleFromQueue.timeUs; - sample.clearData(); - sampleFromQueue.data.position(0).limit(sample.size); - sample.data.put(sampleFromQueue.data); - mSamplePool.releaseSample(sampleFromQueue); - return SampleSource.SAMPLE_READ; - } - - public void clear() { - while (!mQueue.isEmpty()) { - mSamplePool.releaseSample(mQueue.poll()); - } - mLastQueuedPositionUs = null; - } - - public Long getLastQueuedPositionUs() { - return mLastQueuedPositionUs; - } - - public boolean isDurationGreaterThan(long durationUs) { - return !mQueue.isEmpty() && mQueue.getLast().timeUs - mQueue.getFirst().timeUs > durationUs; - } - - public boolean isEmpty() { - return mQueue.isEmpty(); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java deleted file mode 100644 index 159fde18..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.os.ConditionVariable; - -import android.support.annotation.NonNull; - -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.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; -import com.android.tv.tuner.exoplayer.SampleExtractor; - -import java.io.IOException; -import java.util.List; - -/** - * Handles I/O for {@link SampleExtractor} when - * physical storage based buffer is not used. Trickplay is disabled. - */ -public class SimpleSampleBuffer implements BufferManager.SampleBuffer { - private final SamplePool mSamplePool = new SamplePool(); - private SampleQueue[] mPlayingSampleQueues; - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - - private volatile boolean mEos; - - public SimpleSampleBuffer(PlaybackBufferListener bufferListener) { - if (bufferListener != null) { - // Disables trickplay. - bufferListener.onBufferStateChanged(false); - } - } - - @Override - public synchronized void init(@NonNull List<String> ids, - @NonNull List<MediaFormat> mediaFormats) { - int trackCount = ids.size(); - mPlayingSampleQueues = new SampleQueue[trackCount]; - for (int i = 0; i < trackCount; i++) { - mPlayingSampleQueues[i] = null; - } - } - - @Override - public void setEos() { - mEos = true; - } - - private boolean reachedEos() { - return mEos; - } - - @Override - public void selectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] == null) { - mPlayingSampleQueues[index] = new SampleQueue(mSamplePool); - } else { - mPlayingSampleQueues[index].clear(); - } - } - } - - @Override - public void deselectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].clear(); - mPlayingSampleQueues[index] = null; - } - } - } - - @Override - public synchronized long getBufferedPositionUs() { - Long result = null; - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - Long lastQueuedSamplePositionUs = queue.getLastQueuedPositionUs(); - if (lastQueuedSamplePositionUs == null) { - // No sample has been queued. - result = mLastBufferedPositionUs; - continue; - } - if (result == null || result > lastQueuedSamplePositionUs) { - result = lastQueuedSamplePositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } - return (mLastBufferedPositionUs = result); - } - - @Override - public synchronized int readSample(int track, SampleHolder sampleHolder) { - SampleQueue queue = mPlayingSampleQueues[track]; - SoftPreconditions.checkNotNull(queue); - int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder); - if (result != SampleSource.SAMPLE_READ && reachedEos()) { - return SampleSource.END_OF_STREAM; - } - return result; - } - - @Override - public void writeSample(int index, SampleHolder sample, - ConditionVariable conditionVariable) throws IOException { - sample.data.position(0).limit(sample.size); - SampleHolder sampleToQueue = mSamplePool.acquireSample(sample.size); - sampleToQueue.size = sample.size; - sampleToQueue.clearData(); - sampleToQueue.data.put(sample.data); - sampleToQueue.timeUs = sample.timeUs; - sampleToQueue.flags = sample.flags; - - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].queueSample(sampleToQueue); - } - } - } - - @Override - public boolean isWriteSpeedSlow(int sampleSize, long durationNs) { - // Since SimpleSampleBuffer write samples only to memory (not to physical storage), - // write speed is always fine. - return false; - } - - @Override - public void handleWriteSpeedSlow() { - // no-op - } - - @Override - public synchronized boolean continueBuffering(long positionUs) { - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - if (queue.getLastQueuedPositionUs() == null - || positionUs > queue.getLastQueuedPositionUs()) { - // No more buffered data. - return false; - } - } - return true; - } - - @Override - public void seekTo(long positionUs) { - // Not used. - } - - @Override - public void release() { - // Not used. - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java deleted file mode 100644 index 9fe921b8..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.content.Context; -import android.os.AsyncTask; -import android.provider.Settings; -import android.support.annotation.NonNull; -import android.util.Pair; - -import com.android.tv.common.SoftPreconditions; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.SortedMap; - -/** - * Manages Trickplay storage. - */ -public class TrickplayStorageManager implements BufferManager.StorageManager { - // TODO: Support multi-sessions. - private static final String BUFFER_DIR = "timeshift"; - - // Copied from android.provider.Settings.Global (hidden fields) - private static final String - SYS_STORAGE_THRESHOLD_PERCENTAGE = "sys_storage_threshold_percentage"; - private static final String - SYS_STORAGE_THRESHOLD_MAX_BYTES = "sys_storage_threshold_max_bytes"; - - // Copied from android.os.StorageManager - private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; - private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024; - - private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask; - private static File sBufferDir; - private static long sStorageBufferBytes; - - private final long mMaxBufferSize; - - private static void initParamsIfNeeded(Context context, @NonNull File path) { - // TODO: Support multi-sessions. - SoftPreconditions.checkState( - sBufferDir == null || sBufferDir.equals(path)); - if (path.equals(sBufferDir)) { - return; - } - sBufferDir = path; - long lowPercentage = Settings.Global.getInt(context.getContentResolver(), - SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); - long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100; - long maxLowBytes = Settings.Global.getLong(context.getContentResolver(), - SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); - sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes); - } - - public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) { - initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR)); - sBufferDir.mkdirs(); - mMaxBufferSize = maxBufferSize; - clearStorage(); - } - - private void clearStorage() { - long now = System.currentTimeMillis(); - if (sLastCacheCleanUpTask != null) { - sLastCacheCleanUpTask.cancel(true); - } - sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - if (isCancelled()) { - return null; - } - File files[] = sBufferDir.listFiles(); - if (files == null || files.length == 0) { - return null; - } - for (File file : files) { - if (isCancelled()) { - break; - } - long lastModified = file.lastModified(); - if (lastModified != 0 && lastModified < now) { - file.delete(); - } - } - return null; - } - }; - sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public File getBufferDir() { - return sBufferDir; - } - - @Override - public boolean isPersistent() { - return false; - } - - @Override - public boolean reachedStorageMax(long bufferSize, long pendingDelete) { - return bufferSize - pendingDelete > mMaxBufferSize; - } - - @Override - public boolean hasEnoughBuffer(long pendingDelete) { - return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes; - } - - @Override - public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { - return null; - } - - @Override - public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) { - return null; - } - - @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) { - } - - @Override - public void writeIndexFile(String trackName, - SortedMap<Long, Pair<SampleChunk, Integer>> index) { - } - -} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java deleted file mode 100644 index 356636cc..00000000 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java +++ /dev/null @@ -1,249 +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.exoplayer.ffmpeg; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.os.RemoteException; - -import android.support.annotation.MainThread; -import android.support.annotation.WorkerThread; -import android.support.annotation.VisibleForTesting; -import com.google.android.exoplayer.SampleHolder; -import com.android.tv.Features; -import com.android.tv.tuner.exoplayer.audio.AudioDecoder; - -import java.nio.ByteBuffer; - -/** - * The class connects {@link FfmpegDecoderService} to decode audio samples. - * In order to sandbox ffmpeg based decoder, {@link FfmpegDecoderService} is an isolated process - * without any permission and connected by binder. - */ -public class FfmpegDecoderClient extends AudioDecoder { - private static FfmpegDecoderClient sInstance; - - private IFfmpegDecoder mService; - private Boolean mIsAvailable; - - private static final String FFMPEG_DECODER_SERVICE_FILTER = - "com.android.tv.tuner.exoplayer.ffmpeg.IFfmpegDecoder"; - private static final long FFMPEG_SERVICE_CONNECT_TIMEOUT_MS = 500; - - private final ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - mService = IFfmpegDecoder.Stub.asInterface(service); - synchronized (FfmpegDecoderClient.this) { - try { - mIsAvailable = mService.isAvailable(); - } catch (RemoteException e) { - } - FfmpegDecoderClient.this.notify(); - } - } - - @Override - public void onServiceDisconnected(ComponentName className) { - synchronized (FfmpegDecoderClient.this) { - sInstance.releaseLocked(); - mIsAvailable = false; - mService = null; - } - } - }; - - /** - * Connects to the decoder service for future uses. - * @param context - * @return {@code true} when decoder service is connected. - */ - @MainThread - public synchronized static boolean connect(Context context) { - if (Features.AC3_SOFTWARE_DECODE.isEnabled(context)) { - if (sInstance == null) { - sInstance = new FfmpegDecoderClient(); - Intent intent = - new Intent(FFMPEG_DECODER_SERVICE_FILTER) - .setComponent( - new ComponentName(context, FfmpegDecoderService.class)); - if (context.bindService(intent, sInstance.mConnection, Context.BIND_AUTO_CREATE)) { - return true; - } else { - sInstance = null; - } - } - } - return false; - } - - /** - * Disconnects from the decoder service and release resources. - * @param context - */ - @MainThread - public synchronized static void disconnect(Context context) { - if (sInstance != null) { - synchronized (sInstance) { - sInstance.releaseLocked(); - if (sInstance.mIsAvailable != null && sInstance.mIsAvailable) { - context.unbindService(sInstance.mConnection); - } - sInstance.mIsAvailable = false; - sInstance.mService = null; - } - sInstance = null; - } - } - - /** - * Returns whether service is available or not. - * Before using client, this should be used to check availability. - */ - @WorkerThread - public synchronized static boolean isAvailable() { - if (sInstance != null) { - return sInstance.available(); - } - return false; - } - - /** - * Returns an client instance. - */ - public synchronized static FfmpegDecoderClient getInstance() { - if (sInstance != null) { - sInstance.createDecoder(); - } - return sInstance; - } - - private FfmpegDecoderClient() { - } - - private synchronized boolean available() { - if (mIsAvailable == null) { - try { - this.wait(FFMPEG_SERVICE_CONNECT_TIMEOUT_MS); - } catch (InterruptedException e) { - } - } - return mIsAvailable != null && mIsAvailable == true; - } - - private synchronized void createDecoder() { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.create(); - } catch (RemoteException e) { - } - } - - private void releaseLocked() { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.release(); - } catch (RemoteException e) { - } - } - - @Override - public synchronized void release() { - releaseLocked(); - } - - @Override - public synchronized void decode(SampleHolder sampleHolder) { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - byte[] sampleBytes = new byte [sampleHolder.data.limit()]; - sampleHolder.data.get(sampleBytes, 0, sampleBytes.length); - try { - mService.decode(sampleHolder.timeUs, sampleBytes); - } catch (RemoteException e) { - } - } - - @Override - public synchronized void resetDecoderState(String mimeType) { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.resetDecoderState(mimeType); - } catch (RemoteException e) { - } - } - - @Override - public synchronized ByteBuffer getDecodedSample() { - if (mIsAvailable == null || mIsAvailable == false) { - return null; - } - try { - byte[] outputBytes = mService.getDecodedSample(); - if (outputBytes != null && outputBytes.length > 0) { - return ByteBuffer.wrap(outputBytes); - } - } catch (RemoteException e) { - } - return null; - } - - @Override - public synchronized long getDecodedTimeUs() { - if (mIsAvailable == null || mIsAvailable == false) { - return 0; - } - try { - return mService.getDecodedTimeUs(); - } catch (RemoteException e) { - } - return 0; - } - - @VisibleForTesting - public boolean testSandboxIsolatedProcess() { - // When testing isolated process, we will check the permission in FfmpegDecoderService. - // If the service have any permission, an exception will be thrown. - try { - mService.testSandboxIsolatedProcess(); - } catch (RemoteException e) { - return false; - } - return true; - } - - @VisibleForTesting - public void testSandboxMinijail() { - // When testing minijail, we will call a system call which is blocked by minijail. In that - // case, the FfmpegDecoderService will be disconnected, we can check the connection status - // to make sure if the minijail works or not. - try { - mService.testSandboxMinijail(); - } catch (RemoteException e) { - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java deleted file mode 100644 index 3ebdd381..00000000 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java +++ /dev/null @@ -1,205 +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.exoplayer.ffmpeg; - -import android.app.Service; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.AssetFileDescriptor; -import android.os.AsyncTask; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioDecoder; - -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Ffmpeg based audio decoder service. - * It should be isolatedProcess due to security reason. - */ -public class FfmpegDecoderService extends Service { - private static final String TAG = "FfmpegDecoderService"; - private static final boolean DEBUG = false; - - private static final String POLICY_FILE = "whitelist.policy"; - - private static final long MINIJAIL_SETUP_WAIT_TIMEOUT_MS = 5000; - - private static boolean sLibraryLoaded = true; - - static { - try { - System.loadLibrary("minijail_jni"); - } catch (Exception | Error e) { - Log.e(TAG, "Load minijail failed:", e); - sLibraryLoaded = false; - } - } - - private FfmpegDecoder mBinder = new FfmpegDecoder(); - private volatile Object mMinijailSetupMonitor = new Object(); - //@GuardedBy("mMinijailSetupMonitor") - private volatile Boolean mMinijailSetup; - - @Override - public void onCreate() { - if (sLibraryLoaded) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - synchronized (mMinijailSetupMonitor) { - int pipeFd = getPolicyPipeFd(); - if (pipeFd <= 0) { - Log.e(TAG, "fail to open policy file"); - mMinijailSetup = false; - } else { - nativeSetupMinijail(pipeFd); - mMinijailSetup = true; - if (DEBUG) Log.d(TAG, "Minijail setup successfully"); - } - mMinijailSetupMonitor.notify(); - } - return null; - } - }.execute(); - } else { - synchronized (mMinijailSetupMonitor) { - mMinijailSetup = false; - mMinijailSetupMonitor.notify(); - } - } - super.onCreate(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - private int getPolicyPipeFd() { - try { - ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); - final ParcelFileDescriptor.AutoCloseOutputStream outputStream = - new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]); - final AssetFileDescriptor policyFile = getAssets().openFd("whitelist.policy"); - final byte[] buffer = new byte[2048]; - final FileInputStream policyStream = policyFile.createInputStream(); - while (true) { - int bytesRead = policyStream.read(buffer); - if (bytesRead == -1) break; - outputStream.write(buffer, 0, bytesRead); - } - policyStream.close(); - outputStream.close(); - return pipe[0].detachFd(); - } catch (IOException e) { - Log.e(TAG, "Policy file not found:" + e); - } - return -1; - } - - private final class FfmpegDecoder extends IFfmpegDecoder.Stub { - FfmpegAudioDecoder mDecoder; - @Override - public boolean isAvailable() { - return isMinijailSetupDone() && FfmpegAudioDecoder.isAvailable(); - } - - @Override - public void create() { - mDecoder = new FfmpegAudioDecoder(FfmpegDecoderService.this); - } - - @Override - public void release() { - if (mDecoder != null) { - mDecoder.release(); - mDecoder = null; - } - } - - @Override - public void decode(long timeUs, byte[] sample) { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we don't run decode for better security. - return; - } - mDecoder.decode(timeUs, sample); - } - - @Override - public void resetDecoderState(String mimetype) { - mDecoder.resetDecoderState(mimetype); - } - - @Override - public byte[] getDecodedSample() { - ByteBuffer decodedBuffer = mDecoder.getDecodedSample(); - byte[] ret = new byte[decodedBuffer.limit()]; - decodedBuffer.get(ret, 0, ret.length); - return ret; - } - - @Override - public long getDecodedTimeUs() { - return mDecoder.getDecodedTimeUs(); - } - - private boolean isMinijailSetupDone() { - synchronized (mMinijailSetupMonitor) { - if (DEBUG) Log.d(TAG, "mMinijailSetup in isAvailable(): " + mMinijailSetup); - if (mMinijailSetup == null) { - try { - if (DEBUG) Log.d(TAG, "Wait till Minijail setup is done"); - mMinijailSetupMonitor.wait(MINIJAIL_SETUP_WAIT_TIMEOUT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - return mMinijailSetup != null && mMinijailSetup; - } - } - - @Override - public void testSandboxIsolatedProcess() { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we return directly to make the test fail. - return; - } - if (FfmpegDecoderService.this.checkSelfPermission("android.permission.INTERNET") - == PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Shouldn't have the permission of internet"); - } - } - - @Override - public void testSandboxMinijail() { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we return directly to make the test fail. - return; - } - nativeTestMinijail(); - } - } - - private native void nativeSetupMinijail(int policyFd); - private native void nativeTestMinijail(); -} diff --git a/src/com/android/tv/tuner/layout/ScaledLayout.java b/src/com/android/tv/tuner/layout/ScaledLayout.java deleted file mode 100644 index 379ea70e..00000000 --- a/src/com/android/tv/tuner/layout/ScaledLayout.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.layout; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.display.DisplayManager; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.tuner.R; - -import java.util.Arrays; -import java.util.Comparator; - -/** - * A layout that scales its children using the given percentage value. - */ -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; - } - } - }; - - private Rect[] mRectArray; - private final int mMaxWidth; - private final int mMaxHeight; - - public ScaledLayout(Context context) { - this(context, null); - } - - public ScaledLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ScaledLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - Point size = new Point(); - DisplayManager displayManager = (DisplayManager) getContext() - .getSystemService(Context.DISPLAY_SERVICE); - Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - display.getRealSize(size); - mMaxWidth = size.x; - mMaxHeight = size.y; - } - - /** - * ScaledLayoutParams stores the four scale factors. - * <br> - * Vertical coordinate system: ({@code scaleStartRow} * 100) % ~ ({@code scaleEndRow} * 100) % - * Horizontal coordinate system: ({@code scaleStartCol} * 100) % ~ ({@code scaleEndCol} * 100) % - * <br> - * In XML, for example, - * <pre> - * {@code - * <View - * app:layout_scaleStartRow="0.1" - * app:layout_scaleEndRow="0.5" - * app:layout_scaleStartCol="0.4" - * app:layout_scaleEndCol="1" /> - * } - * </pre> - */ - public static class ScaledLayoutParams extends ViewGroup.LayoutParams { - public static final float SCALE_UNSPECIFIED = -1; - public final float scaleStartRow; - public final float scaleEndRow; - public final float scaleStartCol; - public final float scaleEndCol; - - public ScaledLayoutParams(float scaleStartRow, float scaleEndRow, - float scaleStartCol, float scaleEndCol) { - super(MATCH_PARENT, MATCH_PARENT); - this.scaleStartRow = scaleStartRow; - this.scaleEndRow = scaleEndRow; - this.scaleStartCol = scaleStartCol; - this.scaleEndCol = scaleEndCol; - } - - public ScaledLayoutParams(Context context, AttributeSet attrs) { - super(MATCH_PARENT, MATCH_PARENT); - TypedArray array = - context.obtainStyledAttributes(attrs, R.styleable.utScaledLayout); - scaleStartRow = - array.getFloat(R.styleable.utScaledLayout_layout_scaleStartRow, SCALE_UNSPECIFIED); - scaleEndRow = - array.getFloat(R.styleable.utScaledLayout_layout_scaleEndRow, SCALE_UNSPECIFIED); - scaleStartCol = - array.getFloat(R.styleable.utScaledLayout_layout_scaleStartCol, SCALE_UNSPECIFIED); - scaleEndCol = - array.getFloat(R.styleable.utScaledLayout_layout_scaleEndCol, SCALE_UNSPECIFIED); - array.recycle(); - } - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - return new ScaledLayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(LayoutParams p) { - return (p instanceof ScaledLayoutParams); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - int width = widthSpecSize - getPaddingLeft() - getPaddingRight(); - int height = heightSpecSize - getPaddingTop() - getPaddingBottom(); - if (DEBUG) { - Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height)); - } - int count = getChildCount(); - mRectArray = new Rect[count]; - for (int i = 0; i < count; ++i) { - View child = getChildAt(i); - ViewGroup.LayoutParams params = child.getLayoutParams(); - float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol; - if (!(params instanceof ScaledLayoutParams)) { - throw new RuntimeException( - "A child of ScaledLayout cannot have the UNSPECIFIED scale factors"); - } - scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow; - scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow; - scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol; - scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol; - if (scaleStartRow < 0 || scaleStartRow > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleStartRow between 0 and 1"); - } - if (scaleEndRow < scaleStartRow || scaleStartRow > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleEndRow between scaleStartRow and 1"); - } - if (scaleEndCol < 0 || scaleEndCol > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleStartCol between 0 and 1"); - } - if (scaleEndCol < scaleStartCol || scaleEndCol > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleEndCol between scaleStartCol and 1"); - } - if (DEBUG) { - Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f " - + "scaleStartCol: %f scaleEndCol: %f", - scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); - } - mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow * height), - (int) (scaleEndCol * width), (int) (scaleEndRow * height)); - int scaleWidth = (int) (width * (scaleEndCol - scaleStartCol)); - int childWidthSpec = MeasureSpec.makeMeasureSpec( - scaleWidth > mMaxWidth ? mMaxWidth : scaleWidth, MeasureSpec.EXACTLY); - int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - child.measure(childWidthSpec, childHeightSpec); - - // If the height of the measured child view is bigger than the height of the calculated - // region by the given ScaleLayoutParams, the height of the region should be increased - // to fit the size of the child view. - if (child.getMeasuredHeight() > mRectArray[i].height()) { - int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height(); - overflowedHeight = (overflowedHeight + 1) / 2; - mRectArray[i].bottom += overflowedHeight; - mRectArray[i].top -= overflowedHeight; - if (mRectArray[i].top < 0) { - mRectArray[i].bottom -= mRectArray[i].top; - mRectArray[i].top = 0; - } - if (mRectArray[i].bottom > height) { - mRectArray[i].top -= mRectArray[i].bottom - height; - mRectArray[i].bottom = height; - } - } - int scaleHeight = (int) (height * (scaleEndRow - scaleStartRow)); - childHeightSpec = MeasureSpec.makeMeasureSpec( - scaleHeight > mMaxHeight ? mMaxHeight : scaleHeight, MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - } - - // Avoid overlapping rectangles. - // Step 1. Sort rectangles by position (top-left). - int visibleRectCount = 0; - int[] visibleRectGroup = new int[count]; - Rect[] visibleRectArray = new Rect[count]; - for (int i = 0; i < count; ++i) { - if (getChildAt(i).getVisibility() == View.VISIBLE) { - visibleRectGroup[visibleRectCount] = visibleRectCount; - visibleRectArray[visibleRectCount] = mRectArray[i]; - ++visibleRectCount; - } - } - Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter); - - // Step 2. Move down if there are overlapping rectangles. - for (int i = 0; i < visibleRectCount - 1; ++i) { - for (int j = i + 1; j < visibleRectCount; ++j) { - if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) { - visibleRectGroup[j] = visibleRectGroup[i]; - visibleRectArray[j].set(visibleRectArray[j].left, - visibleRectArray[i].bottom, - visibleRectArray[j].right, - visibleRectArray[i].bottom + visibleRectArray[j].height()); - } - } - } - - // Step 3. Move up if there is any overflowed rectangle. - for (int i = visibleRectCount - 1; i >= 0; --i) { - if (visibleRectArray[i].bottom > height) { - int overflowedHeight = visibleRectArray[i].bottom - height; - for (int j = 0; j <= i; ++j) { - if (visibleRectGroup[i] == visibleRectGroup[j]) { - visibleRectArray[j].set(visibleRectArray[j].left, - visibleRectArray[j].top - overflowedHeight, - visibleRectArray[j].right, - visibleRectArray[j].bottom - overflowedHeight); - } - } - } - } - setMeasuredDimension(widthSpecSize, heightSpecSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int paddingLeft = getPaddingLeft(); - int paddingTop = getPaddingTop(); - int count = getChildCount(); - for (int i = 0; i < count; ++i) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - int childLeft = paddingLeft + mRectArray[i].left; - int childTop = paddingTop + mRectArray[i].top; - int childBottom = paddingLeft + mRectArray[i].bottom; - int childRight = paddingTop + mRectArray[i].right; - if (DEBUG) { - Log.d(TAG, String.format("layoutChild bottom: %d left: %d right: %d top: %d", - childBottom, childLeft, - childRight, childTop)); - } - child.layout(childLeft, childTop, childRight, childBottom); - } - } - } -} diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java deleted file mode 100644 index e0e21a20..00000000 --- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; - -import com.android.tv.common.BuildConfig; -import com.android.tv.common.ui.setup.SetupGuidedStepFragment; -import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.tuner.R; - -import java.util.List; -import java.util.TimeZone; - -/** - * A fragment for connection type selection. - */ -public class ConnectionTypeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.ConnectionTypeFragment"; - - @Override - public void onCreate(Bundle savedInstanceState) { - ((TunerSetupActivity) getActivity()).generateTunerHal(); - super.onCreate(savedInstanceState); - } - - @Override - public void onResume() { - ((TunerSetupActivity) getActivity()).generateTunerHal(); - super.onResume(); - } - - @Override - public void onDestroy() { - ((TunerSetupActivity) getActivity()).clearTunerHal(); - super.onDestroy(); - } - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - return new ContentFragment(); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - return new Guidance(getString(R.string.ut_connection_title), - getString(R.string.ut_connection_description), - getString(R.string.ut_setup_breadcrumb), null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices = getResources().getStringArray(R.array.ut_connection_choices); - int length = choices.length - 1; - int startOffset = 0; - for (int i = 0; i < length; ++i) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(startOffset + i) - .title(choices[i]) - .build()); - } - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java deleted file mode 100644 index 025b9193..00000000 --- a/src/com/android/tv/tuner/setup/PostalCodeFragment.java +++ /dev/null @@ -1,178 +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.setup; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.text.InputFilter; -import android.text.InputFilter.AllCaps; -import android.view.View; -import android.widget.TextView; -import com.android.tv.R; -import com.android.tv.common.ui.setup.SetupGuidedStepFragment; -import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.tuner.util.PostalCodeUtils; -import com.android.tv.util.LocationUtils; -import java.util.List; - -/** - * A fragment for initial screen. - */ -public class PostalCodeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.PostalCodeFragment"; - private static final int VIEW_TYPE_EDITABLE = 1; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - ContentFragment fragment = new ContentFragment(); - Bundle arguments = new Bundle(); - arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); - fragment.setArguments(arguments); - return fragment; - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return true; - } - - @Override - protected boolean needsSkipButton() { - return true; - } - - @Override - protected void setOnClickAction(View view, final String category, final int actionId) { - if (actionId == ACTION_DONE) { - view.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - CharSequence postalCode = - ((ContentFragment) getContentFragment()).mEditAction.getTitle(); - String region = LocationUtils.getCurrentCountry(getContext()); - if (postalCode != null && PostalCodeUtils.matches(postalCode, region)) { - PostalCodeUtils.setLastPostalCode( - getContext(), postalCode.toString()); - onActionClick(category, actionId); - } else { - ContentFragment contentFragment = - (ContentFragment) getContentFragment(); - contentFragment.mEditAction.setDescription( - getString(R.string.postal_code_invalid_warning)); - contentFragment.notifyActionChanged(0); - contentFragment.mEditedActionView.performClick(); - } - } - }); - } else if (actionId == ACTION_SKIP) { - super.setOnClickAction(view, category, ACTION_SKIP); - } - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private GuidedAction mEditAction; - private View mEditedActionView; - private View mDoneActionView; - private boolean mProceed; - - @Override - public void onGuidedActionFocused(GuidedAction action) { - if (action.equals(mEditAction)) { - if (mProceed) { - // "NEXT" in IME was just clicked, moves focus to Done button. - if (mDoneActionView == null) { - mDoneActionView = getActivity().findViewById(R.id.button_done); - } - mDoneActionView.requestFocus(); - mProceed = false; - } else { - // Directly opens IME to input postal/zip code. - if (mEditedActionView == null) { - int maxLength = PostalCodeUtils.getRegionMaxLength(getContext()); - mEditedActionView = getView().findViewById(R.id.guidedactions_editable); - ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title)) - .setFilters( - new InputFilter[] { - new InputFilter.LengthFilter(maxLength), new AllCaps() - }); - } - mEditedActionView.performClick(); - } - } - } - - @Override - public long onGuidedActionEditedAndProceed(GuidedAction action) { - mProceed = true; - return 0; - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getString(R.string.postal_code_guidance_title); - String description = getString(R.string.postal_code_guidance_description); - String breadcrumb = getString(R.string.ut_setup_breadcrumb); - return new Guidance(title, description, breadcrumb, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String description = getString(R.string.postal_code_action_description); - mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true) - .description(description).build(); - actions.add(mEditAction); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new GuidedActionsStylist() { - @Override - public int getItemViewType(GuidedAction action) { - if (action.isEditable()) { - return VIEW_TYPE_EDITABLE; - } - return super.getItemViewType(action); - } - - @Override - public int onProvideItemLayoutId(int viewType) { - if (viewType == VIEW_TYPE_EDITABLE) { - return R.layout.guided_action_editable; - } - return super.onProvideItemLayoutId(viewType); - } - }; - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java deleted file mode 100644 index b6936e38..00000000 --- a/src/com/android/tv/tuner/setup/ScanFragment.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.animation.LayoutTransition; -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.ConditionVariable; -import android.os.Handler; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Button; -import android.widget.ListView; -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.data.PsipData; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Channel; -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 java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * A fragment for scanning channels. - */ -public class ScanFragment extends SetupFragment { - private static final String TAG = "ScanFragment"; - private static final boolean DEBUG = false; - - // In the fake mode, the connection to antenna or cable is not necessary. - // Instead dummy channels are added. - private static final boolean FAKE_MODE = false; - - private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d"; - - public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment"; - public static final int ACTION_CANCEL = 1; - public static final int ACTION_FINISH = 2; - - public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice"; - - private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000; - private static final long CHANNEL_SCAN_PERIOD_MS = 4000; - private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300; - - // Build channels out of the locally stored TS streams. - private static final boolean SCAN_LOCAL_STREAMS = true; - - private ChannelDataManager mChannelDataManager; - private ChannelScanTask mChannelScanTask; - private ProgressBar mProgressBar; - private TextView mScanningMessage; - private View mChannelHolder; - private ChannelAdapter mAdapter; - private volatile boolean mChannelListVisible; - private Button mCancelButton; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreateView"); - View view = super.onCreateView(inflater, container, savedInstanceState); - mChannelDataManager = new ChannelDataManager(getActivity()); - mChannelDataManager.checkDataVersion(getActivity()); - mAdapter = new ChannelAdapter(); - mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress); - mScanningMessage = (TextView) view.findViewById(R.id.tune_description); - ListView channelList = (ListView) view.findViewById(R.id.channel_list); - channelList.setAdapter(mAdapter); - channelList.setOnItemClickListener(null); - ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder); - LayoutTransition transition = new LayoutTransition(); - transition.enableTransitionType(LayoutTransition.CHANGING); - progressHolder.setLayoutTransition(transition); - mChannelHolder = view.findViewById(R.id.channel_holder); - mCancelButton = (Button) view.findViewById(R.id.tune_cancel); - mCancelButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - finishScan(false); - } - }); - Bundle args = getArguments(); - int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); - // TODO: Handle the case when the fragment is restored. - 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: - scanTitleView.setText(R.string.ut_channel_scan); - break; - case TunerHal.TUNER_TYPE_NETWORK: - scanTitleView.setText(R.string.nt_channel_scan); - break; - default: - scanTitleView.setText(R.string.bt_channel_scan); - } - return view; - } - - @Override - protected int getLayoutResourceId() { - return R.layout.ut_channel_scan; - } - - @Override - protected int[] getParentIdsForDelay() { - return new int[] {R.id.progress_holder}; - } - - private void startScan(int channelMapId) { - mChannelScanTask = new ChannelScanTask(channelMapId); - mChannelScanTask.execute(); - } - - @Override - public void onPause() { - Log.d(TAG, "onPause"); - if (mChannelScanTask != null) { - // Ensure scan task will stop. - Log.w(TAG, "The activity went to the background. Stopping channel scan."); - mChannelScanTask.stopScan(); - } - super.onPause(); - } - - /** - * Finishes the current scan thread. This fragment will be popped after the scan thread ends. - * - * @param cancel a flag which indicates the scan is canceled or not. - */ - public void finishScan(boolean cancel) { - if (mChannelScanTask != null) { - mChannelScanTask.cancelScan(cancel); - - // Notifies a user of waiting to finish the scanning process. - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (mChannelScanTask != null) { - mChannelScanTask.showFinishingProgressDialog(); - } - } - }, SHOW_PROGRESS_DIALOG_DELAY_MS); - - // Hides the cancel button. - mCancelButton.setEnabled(false); - } - } - - private class ChannelAdapter extends BaseAdapter { - private final ArrayList<TunerChannel> mChannels; - - public ChannelAdapter() { - mChannels = new ArrayList<>(); - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(int pos) { - return false; - } - - @Override - public int getCount() { - return mChannels.size(); - } - - @Override - public Object getItem(int pos) { - return pos; - } - - @Override - public long getItemId(int pos) { - return pos; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final Context context = parent.getContext(); - - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.ut_channel_list, parent, false); - } - - TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num); - channelNum.setText(mChannels.get(position).getDisplayNumber()); - - TextView channelName = (TextView) convertView.findViewById(R.id.channel_name); - channelName.setText(mChannels.get(position).getName()); - return convertView; - } - - public void add(TunerChannel channel) { - mChannels.add(channel); - notifyDataSetChanged(); - } - } - - private class ChannelScanTask extends AsyncTask<Void, Integer, Void> - implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener { - private static final int MAX_PROGRESS = 100; - - private final Activity mActivity; - private final int mChannelMapId; - private final TsStreamer mScanTsStreamer; - private final TsStreamer mFileTsStreamer; - private final ConditionVariable mConditionStopped; - - private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>(); - private boolean mIsCanceled; - private boolean mIsFinished; - private ProgressDialog mFinishingProgressDialog; - private CountDownLatch mLatch; - - public ChannelScanTask(int channelMapId) { - mActivity = getActivity(); - mChannelMapId = channelMapId; - if (FAKE_MODE) { - mScanTsStreamer = new FakeTsStreamer(this); - } else { - TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal(); - if (hal == null) { - throw new RuntimeException("Failed to open a DVB device"); - } - mScanTsStreamer = new TunerTsStreamer(hal, this); - } - mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null; - mConditionStopped = new ConditionVariable(); - mChannelDataManager.setChannelScanListener(this, new Handler()); - } - - 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; - } - } - }); - } - - 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)); - } - } - }); - } - - @Override - protected Void doInBackground(Void... params) { - mScanChannelList.clear(); - if (SCAN_LOCAL_STREAMS) { - FileTsStreamer.addLocalStreamFiles(mScanChannelList); - } - mScanChannelList.addAll(ChannelScanFileParser.parseScanFile( - getResources().openRawResource(mChannelMapId))); - scanChannels(); - return null; - } - - @Override - protected void onCancelled() { - SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel"); - } - - @Override - protected void onProgressUpdate(Integer... values) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mProgressBar.setProgress(values[0], true); - } else { - mProgressBar.setProgress(values[0]); - } - } - - private void stopScan() { - if (mLatch != null) { - mLatch.countDown(); - } - mConditionStopped.open(); - } - - private void cancelScan(boolean cancel) { - mIsCanceled = cancel; - stopScan(); - } - - private void scanChannels() { - if (DEBUG) Log.i(TAG, "Channel scan starting"); - mChannelDataManager.notifyScanStarted(); - - long startMs = System.currentTimeMillis(); - int i = 1; - for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) { - int frequency = scanChannel.frequency; - String modulation = scanChannel.modulation; - Log.i(TAG, "Tuning to " + frequency + " " + modulation); - - TsStreamer streamer = getStreamer(scanChannel.type); - SoftPreconditions.checkNotNull(streamer); - if (streamer != null && streamer.startStream(scanChannel)) { - mLatch = new CountDownLatch(1); - try { - mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.e(TAG, "The current thread is interrupted during scanChannels(). " + - "The TS stream is stopped earlier than expected.", e); - } - streamer.stopStream(); - - addChannelsWithoutVct(scanChannel); - if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS - && !mChannelListVisible) { - maybeSetChannelListVisible(); - } - } - if (mConditionStopped.block(-1)) { - break; - } - publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size()); - } - mChannelDataManager.notifyScanCompleted(); - if (!mConditionStopped.block(-1)) { - publishProgress(MAX_PROGRESS); - } - if (DEBUG) Log.i(TAG, "Channel scan ended"); - } - - - private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) { - if (scanChannel.radioFrequencyNumber == null - || !(mScanTsStreamer instanceof TunerTsStreamer)) { - return; - } - for (TunerChannel tunerChannel - : ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) { - if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID) - && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) { - tunerChannel.setFrequency(scanChannel.frequency); - tunerChannel.setModulation(scanChannel.modulation); - tunerChannel.setShortName(String.format(Locale.US, VCTLESS_CHANNEL_NAME_FORMAT, - scanChannel.radioFrequencyNumber, - tunerChannel.getProgramNumber())); - tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber); - tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber()); - onChannelDetected(tunerChannel, true); - } - } - } - - private TsStreamer getStreamer(int type) { - switch (type) { - case Channel.TYPE_TUNER: - return mScanTsStreamer; - case Channel.TYPE_FILE: - return mFileTsStreamer; - default: - return null; - } - } - - @Override - public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { - mChannelDataManager.notifyEventDetected(channel, items); - } - - @Override - public void onChannelScanDone() { - if (mLatch != null) { - mLatch.countDown(); - } - } - - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (channelArrivedAtFirstTime) { - Log.i(TAG, "Found channel " + channel); - } - if (channelArrivedAtFirstTime && channel.hasAudio()) { - // Playbacks with video-only stream have not been tested yet. - // No video-only channel has been found. - addChannel(channel); - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - } - - public void showFinishingProgressDialog() { - // Show a progress dialog to wait for the scanning process if it's not done yet. - if (!mIsFinished && mFinishingProgressDialog == null) { - mFinishingProgressDialog = ProgressDialog.show(mActivity, "", - getString(R.string.ut_setup_cancel), true, false); - } - } - - @Override - public void onChannelHandlingDone() { - mChannelDataManager.setCurrentVersion(mActivity); - mChannelDataManager.releaseSafely(); - mIsFinished = true; - TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(), - mChannelDataManager.getScannedChannelCount()); - // Cancel a previously shown notification. - TunerSetupActivity.cancelNotification(mActivity.getApplicationContext()); - // Mark scan as done - TunerPreferences.setScanDone(mActivity.getApplicationContext()); - // finishing will be done manually. - if (mFinishingProgressDialog != null) { - mFinishingProgressDialog.dismiss(); - } - // If the fragment is not resumed, the next fragment (scan result page) can't be - // displayed. In that case, just close the activity. - if (isResumed()) { - onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); - } else if (getActivity() != null) { - getActivity().finish(); - } - mChannelScanTask = null; - } - } - - private static class FakeTsStreamer implements TsStreamer { - private final EventDetector.EventListener mEventListener; - private int mProgramNumber = 0; - - FakeTsStreamer(EventDetector.EventListener eventListener) { - mEventListener = eventListener; - } - - @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (++mProgramNumber % 2 == 1) { - return true; - } - final String displayNumber = Integer.toString(mProgramNumber); - final String name = "Channel-" + mProgramNumber; - mEventListener.onChannelDetected(new TunerChannel(mProgramNumber, new ArrayList<>()) { - @Override - public String getDisplayNumber() { - return displayNumber; - } - - @Override - public String getName() { - return name; - } - }, true); - return true; - } - - @Override - public boolean startStream(TunerChannel channel) { - return false; - } - - @Override - public void stopStream() { - } - - @Override - public TsDataSource createDataSource() { - return null; - } - } -} diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java deleted file mode 100644 index 3b8cd823..00000000 --- a/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.content.Context; -import android.content.res.Resources; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -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.util.TunerInputInfoUtils; - -import java.util.List; - -/** - * A fragment for initial screen. - */ -public class ScanResultFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.ScanResultFragment"; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - return new ContentFragment(); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private int mChannelCountOnPreference; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mChannelCountOnPreference = TunerPreferences.getScannedChannelCount(context); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title; - String description; - String breadcrumb; - if (mChannelCountOnPreference > 0) { - Resources res = getResources(); - title = res.getQuantityString(R.plurals.ut_result_found_title, - mChannelCountOnPreference, mChannelCountOnPreference); - description = res.getQuantityString(R.plurals.ut_result_found_description, - mChannelCountOnPreference, mChannelCountOnPreference); - breadcrumb = null; - } else { - Bundle args = getArguments(); - int tunerType = - (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0)); - title = getString(R.string.ut_result_not_found_title); - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - description = getString(R.string.ut_result_not_found_description); - break; - case TunerHal.TUNER_TYPE_NETWORK: - description = getString(R.string.nt_result_not_found_description); - break; - default: - description = getString(R.string.bt_result_not_found_description); - } - breadcrumb = getString(R.string.ut_setup_breadcrumb); - } - return new Guidance(title, description, breadcrumb, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices; - int doneActionIndex; - if (mChannelCountOnPreference > 0) { - choices = getResources().getStringArray(R.array.ut_result_found_choices); - doneActionIndex = 0; - } else { - choices = getResources().getStringArray(R.array.ut_result_not_found_choices); - doneActionIndex = 1; - } - for (int i = 0; i < choices.length; ++i) { - if (i == doneActionIndex) { - actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DONE) - .title(choices[i]).build()); - } else { - actions.add(new GuidedAction.Builder(getActivity()).id(i).title(choices[i]) - .build()); - } - } - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java deleted file mode 100644 index e9f3baa7..00000000 --- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java +++ /dev/null @@ -1,543 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.tv.TvContract; -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.view.KeyEvent; -import android.widget.Toast; - -import com.android.tv.Features; -import com.android.tv.TvApplication; -import com.android.tv.common.AutoCloseableUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonConstants; -import com.android.tv.common.TvCommonUtils; -import com.android.tv.common.ui.setup.SetupActivity; -import com.android.tv.common.ui.setup.SetupFragment; -import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.experiments.Experiments; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.tvinput.TunerTvInputService; -import com.android.tv.tuner.util.PostalCodeUtils; - -import java.util.concurrent.Executor; - -/** - * An activity that serves tuner setup process. - */ -public class TunerSetupActivity extends SetupActivity { - private static final String TAG = "TunerSetupActivity"; - private static final boolean DEBUG = false; - - /** - * Key for passing tuner type to sub-fragments. - */ - public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType"; - - // For the notification. - private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; - private static final String TUNER_SET_UP_NOTIFICATION_CHANNEL_ID = "tuner_setup_channel"; - private static final String NOTIFY_TAG = "TunerSetup"; - private static final int NOTIFY_ID = 1000; - private static final String TAG_DRAWABLE = "drawable"; - private static final String TAG_ICON = "ic_launcher_s"; - private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1; - - private static final int CHANNEL_MAP_SCAN_FILE[] = { - R.raw.ut_us_atsc_center_frequencies_8vsb, - R.raw.ut_us_cable_standard_center_frequencies_qam256, - R.raw.ut_us_all, - R.raw.ut_kr_atsc_center_frequencies_8vsb, - R.raw.ut_kr_cable_standard_center_frequencies_qam256, - R.raw.ut_kr_all, - R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256, - R.raw.ut_euro_dvbt_all, - R.raw.ut_euro_dvbt_all, - R.raw.ut_euro_dvbt_all - }; - - private ScanFragment mLastScanFragment; - private Integer mTunerType; - private TunerHalFactory mTunerHalFactory; - private boolean mNeedToShowPostalCodeFragment; - private String mPreviousPostalCode; - - @Override - protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - new AsyncTask<Void, Void, Integer>() { - @Override - protected Integer doInBackground(Void... arg0) { - return TunerHal.getTunerTypeAndCount(TunerSetupActivity.this).first; - } - - @Override - protected void onPostExecute(Integer result) { - if (!TunerSetupActivity.this.isDestroyed()) { - mTunerType = result; - if (result == null) { - finish(); - } else { - showInitialFragment(); - } - } - } - }.execute(); - TvApplication.setCurrentRunningProcess(this, false); - 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); - } - mTunerHalFactory = new TunerHalFactory(getApplicationContext()); - try { - // Updating postal code takes time, therefore we called it here for "warm-up". - mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this); - PostalCodeUtils.setLastPostalCode(this, null); - PostalCodeUtils.updatePostalCode(this); - } catch (Exception e) { - // Do nothing. If the last known postal code is null, we'll show guided fragment to - // prompt users to input postal code before ConnectionTypeFragment is shown. - Log.i(TAG, "Can't get postal code:" + e); - } - } - - @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(); - Bundle args = new Bundle(); - args.putInt(KEY_TUNER_TYPE, mTunerType); - fragment.setArguments(args); - fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION - | SetupFragment.FRAGMENT_REENTER_TRANSITION); - return fragment; - } else { - return null; - } - } - - @Override - protected boolean executeAction(String category, int actionId, Bundle params) { - switch (category) { - case WelcomeFragment.ACTION_CATEGORY: - switch (actionId) { - case SetupMultiPaneFragment.ACTION_DONE: - // If the scan was performed, then the result should be OK. - setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK); - finish(); - break; - default: - if (mNeedToShowPostalCodeFragment - || Features.ENABLE_CLOUD_EPG_REGION.isEnabled( - getApplicationContext()) - && TextUtils.isEmpty( - PostalCodeUtils.getLastPostalCode(this))) { - // We cannot get postal code automatically. Postal code input fragment - // should always be shown even if users have input some valid postal - // code in this activity before. - mNeedToShowPostalCodeFragment = true; - showPostalCodeFragment(); - } else { - showConnectionTypeFragment(); - } - break; - } - return true; - case PostalCodeFragment.ACTION_CATEGORY: - if (actionId == SetupMultiPaneFragment.ACTION_DONE - || actionId == SetupMultiPaneFragment.ACTION_SKIP) { - showConnectionTypeFragment(); - } - return true; - case ConnectionTypeFragment.ACTION_CATEGORY: - if (mTunerHalFactory.getOrCreate() == null) { - finish(); - Toast.makeText(getApplicationContext(), - R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); - return true; - } - mLastScanFragment = new ScanFragment(); - Bundle args1 = new Bundle(); - args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, - CHANNEL_MAP_SCAN_FILE[actionId]); - args1.putInt(KEY_TUNER_TYPE, mTunerType); - mLastScanFragment.setArguments(args1); - showFragment(mLastScanFragment, true); - return true; - case ScanFragment.ACTION_CATEGORY: - switch (actionId) { - case ScanFragment.ACTION_CANCEL: - getFragmentManager().popBackStack(); - return true; - case ScanFragment.ACTION_FINISH: - mTunerHalFactory.clear(); - SetupFragment fragment = new ScanResultFragment(); - Bundle args2 = new Bundle(); - args2.putInt(KEY_TUNER_TYPE, mTunerType); - fragment.setArguments(args2); - fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION - | SetupFragment.FRAGMENT_REENTER_TRANSITION); - showFragment(fragment, true); - return true; - } - break; - case ScanResultFragment.ACTION_CATEGORY: - switch (actionId) { - case SetupMultiPaneFragment.ACTION_DONE: - setResult(RESULT_OK); - finish(); - break; - default: - SetupFragment fragment = new ConnectionTypeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - break; - } - return true; - } - return false; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - FragmentManager manager = getFragmentManager(); - int count = manager.getBackStackEntryCount(); - if (count > 0) { - String lastTag = manager.getBackStackEntryAt(count - 1).getName(); - if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { - // Pops fragment including ScanFragment. - manager.popBackStack(manager.getBackStackEntryAt(count - 2).getName(), - FragmentManager.POP_BACK_STACK_INCLUSIVE); - return true; - } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) { - mLastScanFragment.finishScan(true); - return true; - } - } - } - return super.onKeyUp(keyCode, event); - } - - @Override - public void onDestroy() { - if (mPreviousPostalCode != null && PostalCodeUtils.getLastPostalCode(this) == null) { - PostalCodeUtils.setLastPostalCode(this, mPreviousPostalCode); - } - super.onDestroy(); - } - - /** - * A callback to be invoked when the TvInputService is enabled or disabled. - * - * @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) { - // 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); - } else { - TunerPreferences.setShouldShowSetupActivity(context, false); - cancelNotification(context); - } - } - - /** - * Returns a {@link Intent} to launch the tuner TV input service. - * - * @param context a {@link Context} instance - */ - public static Intent createSetupActivity(Context context) { - String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), - TunerTvInputService.class.getName())); - - // Make an intent to launch the setup activity of TV tuner input. - Intent intent = TvCommonUtils.createSetupIntent( - new Intent(context, TunerSetupActivity.class), inputId); - intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); - Intent tvActivityIntent = new Intent(); - tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); - intent.putExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); - return intent; - } - - /** - * Gets the currently used tuner HAL. - */ - TunerHal getTunerHal() { - return mTunerHalFactory.getOrCreate(); - } - - /** - * Generates tuner HAL. - */ - void generateTunerHal() { - mTunerHalFactory.generate(); - } - - /** - * Clears the currently used tuner HAL. - */ - void clearTunerHal() { - mTunerHalFactory.clear(); - } - - /** - * Returns a {@link PendingIntent} to launch the tuner TV input service. - * - * @param context a {@link Context} instance - */ - private static PendingIntent createPendingIntentForSetupActivity(Context context) { - return PendingIntent.getActivity(context, 0, createSetupActivity(context), - PendingIntent.FLAG_UPDATE_CURRENT); - } - - private static void sendNotification(Context context, Integer tunerType) { - SoftPreconditions.checkState(tunerType != null, TAG, - "tunerType is null when send notification"); - if (tunerType == null) { - return; - } - Resources resources = context.getResources(); - String contentTitle = resources.getString(R.string.ut_setup_notification_content_title); - int contentTextId = 0; - switch (tunerType) { - case TunerHal.TUNER_TYPE_BUILT_IN: - contentTextId = R.string.bt_setup_notification_content_text; - break; - case TunerHal.TUNER_TYPE_USB: - contentTextId = R.string.ut_setup_notification_content_text; - break; - case TunerHal.TUNER_TYPE_NETWORK: - contentTextId = R.string.nt_setup_notification_content_text; - break; - } - String contentText = resources.getString(contentTextId); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - sendNotificationInternal(context, contentTitle, contentText); - } else { - Bitmap largeIcon = BitmapFactory.decodeResource(resources, - R.drawable.recommendation_antenna); - sendRecommendationCard(context, contentTitle, contentText, largeIcon); - } - } - - /** - * Sends the recommendation card to start the tuner TV input setup activity. - * - * @param context a {@link Context} instance - */ - private static void sendRecommendationCard(Context context, String contentTitle, - String contentText, Bitmap largeIcon) { - // Build and send the notification. - Notification notification = new NotificationCompat.BigPictureStyle( - new NotificationCompat.Builder(context) - .setAutoCancel(false) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setContentInfo(contentText) - .setCategory(Notification.CATEGORY_RECOMMENDATION) - .setLargeIcon(largeIcon) - .setSmallIcon(context.getResources().getIdentifier( - TAG_ICON, TAG_DRAWABLE, context.getPackageName())) - .setContentIntent(createPendingIntentForSetupActivity(context))) - .build(); - NotificationManager notificationManager = (NotificationManager) context - .getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); - } - - private static void sendNotificationInternal(Context context, String contentTitle, - String contentText) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService( - Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(new NotificationChannel( - TUNER_SET_UP_NOTIFICATION_CHANNEL_ID, - context.getResources().getString(R.string.ut_setup_notification_channel_name), - NotificationManager.IMPORTANCE_HIGH)); - Notification notification = new Notification.Builder( - context, TUNER_SET_UP_NOTIFICATION_CHANNEL_ID) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setSmallIcon(context.getResources().getIdentifier( - TAG_ICON, TAG_DRAWABLE, context.getPackageName())) - .setContentIntent(createPendingIntentForSetupActivity(context)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .extend(new Notification.TvExtender()) - .build(); - notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); - } - - private void showPostalCodeFragment() { - SetupFragment fragment = new PostalCodeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - } - - private void showConnectionTypeFragment() { - SetupFragment fragment = new ConnectionTypeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - } - - /** - * Cancels the previously shown notification. - * - * @param context a {@link Context} instance - */ - public static void cancelNotification(Context context) { - NotificationManager notificationManager = (NotificationManager) context - .getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID); - } - - @VisibleForTesting - static class TunerHalFactory { - private Context mContext; - @VisibleForTesting - TunerHal mTunerHal; - private GenerateTunerHalTask mGenerateTunerHalTask; - private final Executor mExecutor; - - TunerHalFactory(Context context) { - this(context, AsyncTask.SERIAL_EXECUTOR); - } - - TunerHalFactory(Context context, Executor executor) { - mContext = context; - mExecutor = executor; - } - - /** - * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated - * before, tries to generate it synchronously. - */ - @WorkerThread - TunerHal getOrCreate() { - if (mGenerateTunerHalTask != null - && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) { - try { - return mGenerateTunerHalTask.get(); - } catch (Exception e) { - Log.e(TAG, "Cannot get Tuner HAL: " + e); - } - } else if (mGenerateTunerHalTask == null && mTunerHal == null) { - mTunerHal = createInstance(); - } - return mTunerHal; - } - - /** - * Generates tuner hal for scanning with asynchronous tasks. - */ - @MainThread - void generate() { - if (mGenerateTunerHalTask == null && mTunerHal == null) { - mGenerateTunerHalTask = new GenerateTunerHalTask(); - mGenerateTunerHalTask.executeOnExecutor(mExecutor); - } - } - - /** - * Clears the currently used tuner hal. - */ - @MainThread - void clear() { - if (mGenerateTunerHalTask != null) { - mGenerateTunerHalTask.cancel(true); - mGenerateTunerHalTask = null; - } - if (mTunerHal != null) { - AutoCloseableUtils.closeQuietly(mTunerHal); - mTunerHal = null; - } - } - - @WorkerThread - protected TunerHal createInstance() { - return TunerHal.createInstance(mContext); - } - - class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> { - @Override - protected TunerHal doInBackground(Void... args) { - return createInstance(); - } - - @Override - protected void onPostExecute(TunerHal tunerHal) { - mTunerHal = tunerHal; - } - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java deleted file mode 100644 index feae1ec9..00000000 --- a/src/com/android/tv/tuner/setup/WelcomeFragment.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.setup; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -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 java.util.List; - -/** - * A fragment for initial screen. - */ -public class WelcomeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.WelcomeFragment"; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - ContentFragment fragment = new ContentFragment(); - fragment.setArguments(getArguments()); - return fragment; - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private int mChannelCountOnPreference; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - mChannelCountOnPreference = - TunerPreferences.getScannedChannelCount(getActivity().getApplicationContext()); - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title; - String description; - int tunerType = getArguments().getInt(TunerSetupActivity.KEY_TUNER_TYPE, - TunerHal.TUNER_TYPE_BUILT_IN); - if (mChannelCountOnPreference == 0) { - switch (tunerType) { - case TunerHal.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: - title = getString(R.string.nt_setup_new_title); - description = getString(R.string.nt_setup_new_description); - break; - default: - title = getString(R.string.bt_setup_new_title); - description = getString(R.string.bt_setup_new_description); - } - } else { - title = getString(R.string.bt_setup_again_title); - switch (tunerType) { - case TunerHal.TUNER_TYPE_USB: - description = getString(R.string.ut_setup_again_description); - break; - case TunerHal.TUNER_TYPE_NETWORK: - description = getString(R.string.nt_setup_again_description); - break; - default: - description = getString(R.string.bt_setup_again_description); - } - } - return new Guidance(title, description, null, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices = getResources().getStringArray(mChannelCountOnPreference == 0 - ? R.array.ut_setup_new_choices : R.array.ut_setup_again_choices); - for (int i = 0; i < choices.length - 1; ++i) { - actions.add(new GuidedAction.Builder(getActivity()).id(i).title(choices[i]) - .build()); - } - actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DONE) - .title(choices[choices.length - 1]).build()); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java deleted file mode 100644 index f17dd46b..00000000 --- a/src/com/android/tv/tuner/source/FileTsStreamer.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import android.content.Context; -import android.os.Environment; -import android.util.Log; -import android.util.SparseBooleanArray; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSpec; -import com.android.tv.Features; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.ChannelScanFileParser.ScanChannel; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.FileSourceEventDetector; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Provides MPEG-2 TS stream sources for both channel scanning and channel playing from a local file - * generated by capturing TV signal. - */ -public class FileTsStreamer implements TsStreamer { - private static final String TAG = "FileTsStreamer"; - - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; - private static final int MIN_READ_UNIT = TS_PACKET_SIZE * 10; - private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~20KB - private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 4000; // ~ 8MB - private static final int PADDING_SIZE = MIN_READ_UNIT * 1000; // ~2MB - private static final int READ_TIMEOUT_MS = 10000; // 10 secs. - private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; - private static final String FILE_DIR = - new File(Environment.getExternalStorageDirectory(), "Streams").getAbsolutePath(); - - // Virtual frequency base used for file-based source - public static final int FREQ_BASE = 100; - - private final Object mCircularBufferMonitor = new Object(); - private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; - private final FileSourceEventDetector mEventDetector; - private final Context mContext; - - private long mBytesFetched; - private long mLastReadPosition; - private boolean mStreaming; - - private Thread mStreamingThread; - private StreamProvider mSource; - - public static class FileDataSource extends TsDataSource { - private final FileTsStreamer mTsStreamer; - private final AtomicLong mLastReadPosition = new AtomicLong(0); - private long mStartBufferedPosition; - - private FileDataSource(FileTsStreamer tsStreamer) { - mTsStreamer = tsStreamer; - mStartBufferedPosition = tsStreamer.getBufferedPosition(); - } - - @Override - public long getBufferedPosition() { - return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; - } - - @Override - public long getLastReadPosition() { - return mLastReadPosition.get(); - } - - @Override - public void shiftStartPosition(long offset) { - SoftPreconditions.checkState(mLastReadPosition.get() == 0); - SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition()); - mStartBufferedPosition += offset; - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - mLastReadPosition.set(0); - return C.LENGTH_UNBOUNDED; - } - - @Override - public void close() { - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, - offset, readLength); - if (ret > 0) { - mLastReadPosition.addAndGet(ret); - } - return ret; - } - } - - /** - * Creates {@link TsStreamer} for scanning & playing MPEG-2 TS file. - * @param eventListener the listener for channel & program information - */ - public FileTsStreamer(EventDetector.EventListener eventListener, Context context) { - mEventDetector = - new FileSourceEventDetector( - eventListener, Features.ENABLE_FILE_DVB.isEnabled(context)); - mContext = context; - } - - @Override - public boolean startStream(ScanChannel channel) { - String filepath = new File(FILE_DIR, channel.filename).getAbsolutePath(); - mSource = new StreamProvider(filepath); - if (!mSource.isReady()) { - return false; - } - mEventDetector.start(mSource, FileSourceEventDetector.ALL_PROGRAM_NUMBERS); - mSource.addPidFilter(TsParser.PAT_PID); - mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID); - if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) { - mSource.addPidFilter(TsParser.DVB_EIT_PID); - mSource.addPidFilter(TsParser.DVB_SDT_PID); - } - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - return true; - } - mStreaming = true; - } - - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - - @Override - public boolean startStream(TunerChannel channel) { - Log.i(TAG, "tuneToChannel with: " + channel.getFilepath()); - mSource = new StreamProvider(channel.getFilepath()); - if (!mSource.isReady()) { - return false; - } - mEventDetector.start(mSource, channel.getProgramNumber()); - mSource.addPidFilter(channel.getVideoPid()); - for (Integer i : channel.getAudioPids()) { - mSource.addPidFilter(i); - } - mSource.addPidFilter(channel.getPcrPid()); - mSource.addPidFilter(TsParser.PAT_PID); - mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID); - if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) { - mSource.addPidFilter(TsParser.DVB_EIT_PID); - mSource.addPidFilter(TsParser.DVB_SDT_PID); - } - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - return true; - } - mStreaming = true; - } - - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - - /** - * Blocks the current thread until the streaming thread stops. In rare cases when the tuner - * device is overloaded this can take a while, but usually it returns pretty quickly. - */ - @Override - public void stopStream() { - synchronized (mCircularBufferMonitor) { - mStreaming = false; - mCircularBufferMonitor.notify(); - } - - try { - if (mStreamingThread != null) { - mStreamingThread.join(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Override - public TsDataSource createDataSource() { - return new FileDataSource(this); - } - - /** - * Returns the current buffered position from the file. - * @return the current buffered position - */ - public long getBufferedPosition() { - synchronized (mCircularBufferMonitor) { - return mBytesFetched; - } - } - - /** - * Provides MPEG-2 transport stream from a local file. Stream can be filtered by PID. - */ - public static class StreamProvider { - private final String mFilepath; - private final SparseBooleanArray mPids = new SparseBooleanArray(); - private final byte[] mPreBuffer = new byte[READ_BUFFER_SIZE]; - - private BufferedInputStream mInputStream; - - private StreamProvider(String filepath) { - mFilepath = filepath; - open(filepath); - } - - private void open(String filepath) { - try { - mInputStream = new BufferedInputStream(new FileInputStream(filepath)); - } catch (IOException e) { - Log.e(TAG, "Error opening input stream", e); - mInputStream = null; - } - } - - private boolean isReady() { - return mInputStream != null; - } - - /** - * Returns the file path of the MPEG-2 TS file. - */ - public String getFilepath() { - return mFilepath; - } - - /** - * Adds a pid for filtering from the MPEG-2 TS file. - */ - public void addPidFilter(int pid) { - mPids.put(pid, true); - } - - /** - * Returns whether the current pid filter is empty or not. - */ - public boolean isFilterEmpty() { - return mPids.size() == 0; - } - - /** - * Clears the current pid filter. - */ - public void clearPidFilter() { - mPids.clear(); - } - - /** - * Returns whether a pid is in the pid filter or not. - * @param pid the pid to check - */ - public boolean isInFilter(int pid) { - return mPids.get(pid); - } - - /** - * Reads from the MPEG-2 TS file to buffer. - * - * @param inputBuffer to read - * @return the number of read bytes - */ - private int read(byte[] inputBuffer) { - int readSize = readInternal(); - if (readSize <= 0) { - // Reached the end of stream. Restart from the beginning. - close(); - open(mFilepath); - if (mInputStream == null) { - return -1; - } - readSize = readInternal(); - } - - if (mPreBuffer[0] != TS_SYNC_BYTE) { - Log.e(TAG, "Error reading input stream - no TS sync found"); - return -1; - } - int filteredSize = 0; - for (int i = 0, destPos = 0; i < readSize; i += TS_PACKET_SIZE) { - if (mPreBuffer[i] == TS_SYNC_BYTE) { - int pid = ((mPreBuffer[i + 1] & 0x1f) << 8) + (mPreBuffer[i + 2] & 0xff); - if (mPids.get(pid)) { - System.arraycopy(mPreBuffer, i, inputBuffer, destPos, TS_PACKET_SIZE); - destPos += TS_PACKET_SIZE; - filteredSize += TS_PACKET_SIZE; - } - } - } - return filteredSize; - } - - private int readInternal() { - int readSize; - try { - readSize = mInputStream.read(mPreBuffer, 0, mPreBuffer.length); - } catch (IOException e) { - Log.e(TAG, "Error reading input stream", e); - return -1; - } - return readSize; - } - - private void close() { - try { - mInputStream.close(); - } catch (IOException e) { - Log.e(TAG, "Error closing input stream:", e); - } - mInputStream = null; - } - } - - /** - * Reads data from internal buffer. - * @param pos the position to read from - * @param buffer to read - * @param offset start position of the read buffer - * @param amount number of bytes to read - * @return number of read bytes when successful, {@code -1} otherwise - * @throws IOException - */ - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - synchronized (mCircularBufferMonitor) { - long initialBytesFetched = mBytesFetched; - while (mBytesFetched < pos + amount && mStreaming) { - try { - mCircularBufferMonitor.wait(READ_TIMEOUT_MS); - } catch (InterruptedException e) { - // Wait again. - Thread.currentThread().interrupt(); - } - if (initialBytesFetched == mBytesFetched) { - Log.w(TAG, "No data update for " + READ_TIMEOUT_MS + "ms. returning -1."); - - // Returning -1 will make demux report EOS so that the input service can retry - // the playback. - return -1; - } - } - if (!mStreaming) { - Log.w(TAG, "Stream is already stopped."); - return -1; - } - if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.e(TAG, "Demux is requesting the data which is already overwritten."); - return -1; - } - int posInBuffer = (int) (pos % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = amount; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(mCircularBuffer, posInBuffer, buffer, offset, bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < amount) { - System.arraycopy(mCircularBuffer, 0, buffer, offset + bytesToCopyInFirstPass, - amount - bytesToCopyInFirstPass); - } - mLastReadPosition = pos + amount; - mCircularBufferMonitor.notify(); - return amount; - } - } - - /** - * Adds {@link ScanChannel} instance for local files. - * - * @param output a list of channels where the results will be placed in - */ - public static void addLocalStreamFiles(List<ScanChannel> output) { - File dir = new File(FILE_DIR); - if (!dir.exists()) return; - - File[] tsFiles = dir.listFiles(); - if (tsFiles == null) return; - int freq = FileTsStreamer.FREQ_BASE; - for (File file : tsFiles) { - if (!file.isFile()) continue; - output.add(ScanChannel.forFile(freq, file.getName())); - freq += 100; - } - } - - /** - * A thread managing a circular buffer that holds stream data to be consumed by player. - * Keeps reading data in from a {@link StreamProvider} to hold enough amount for buffering. - * Started and stopped by {@link #startStream()} and {@link #stopStream()}, respectively. - */ - private class StreamingThread extends Thread { - @Override - public void run() { - byte[] dataBuffer = new byte[READ_BUFFER_SIZE]; - - synchronized (mCircularBufferMonitor) { - mBytesFetched = 0; - mLastReadPosition = 0; - } - - while (true) { - synchronized (mCircularBufferMonitor) { - while ((mBytesFetched - mLastReadPosition + PADDING_SIZE) > CIRCULAR_BUFFER_SIZE - && mStreaming) { - try { - mCircularBufferMonitor.wait(); - } catch (InterruptedException e) { - // Wait again. - Thread.currentThread().interrupt(); - } - } - if (!mStreaming) { - break; - } - } - - int bytesWritten = mSource.read(dataBuffer); - if (bytesWritten <= 0) { - try { - // When buffer is underrun, we sleep for short time to prevent - // unnecessary CPU draining. - sleep(BUFFER_UNDERRUN_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - continue; - } - - mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); - - synchronized (mCircularBufferMonitor) { - int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = bytesWritten; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(dataBuffer, 0, mCircularBuffer, posInBuffer, - bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < bytesWritten) { - System.arraycopy(dataBuffer, bytesToCopyInFirstPass, mCircularBuffer, 0, - bytesWritten - bytesToCopyInFirstPass); - } - mBytesFetched += bytesWritten; - mCircularBufferMonitor.notify(); - } - } - - Log.i(TAG, "Streaming stopped"); - mSource.close(); - } - } -} diff --git a/src/com/android/tv/tuner/source/TsDataSource.java b/src/com/android/tv/tuner/source/TsDataSource.java deleted file mode 100644 index 2ce3e670..00000000 --- a/src/com/android/tv/tuner/source/TsDataSource.java +++ /dev/null @@ -1,50 +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.source; - -import com.google.android.exoplayer.upstream.DataSource; - -/** - * {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. - */ -public abstract class TsDataSource implements DataSource { - - /** - * Returns the number of bytes being buffered by {@link TsStreamer} so far. - * - * @return the buffered position - */ - public long getBufferedPosition() { - return 0; - } - - /** - * Returns the offset position where the last {@link DataSource#read} read. - * - * @return the last read position - */ - public long getLastReadPosition() { - return 0; - } - - /** - * Shifts start position by the specified offset. - * Do not call this method when the class already provided MPEG-TS stream to the extractor. - * @param offset 0 <= offset <= buffered position - */ - public void shiftStartPosition(long offset) { } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java deleted file mode 100644 index 16be7582..00000000 --- a/src/com/android/tv/tuner/source/TsDataSourceManager.java +++ /dev/null @@ -1,143 +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.source; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; - -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Channel; -import com.android.tv.tuner.tvinput.EventDetector; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 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. - */ -public class TsDataSourceManager { - private static final Object sLock = new Object(); - private static final Map<TsDataSource, TsStreamer> sTsStreamers = - new ConcurrentHashMap<>(); - - private static int sSequenceId; - - private final int mId; - private final boolean mIsRecording; - private final TunerTsStreamerManager mTunerStreamerManager = - TunerTsStreamerManager.getInstance(); - - private boolean mKeepTuneStatus; - - /** - * Creates TsDataSourceManager to create and release {@link DataSource} which will be - * used for playing and recording. - * @param isRecording {@code true} when for recording, {@code false} otherwise - * @return {@link TsDataSourceManager} - */ - public static TsDataSourceManager createSourceManager(boolean isRecording) { - int id; - synchronized (sLock) { - id = ++sSequenceId; - } - return new TsDataSourceManager(id, isRecording); - } - - private TsDataSourceManager(int id, boolean isRecording) { - mId = id; - mIsRecording = isRecording; - mKeepTuneStatus = true; - } - - /** - * Creates or retrieves {@link TsDataSource} for playing or recording - * @param context a {@link Context} instance - * @param channel to play or record - * @param eventListener for program information which will be scanned from MPEG2-TS stream - * @return {@link TsDataSource} which will provide the specified channel stream - */ - public TsDataSource createDataSource(Context context, TunerChannel channel, - EventDetector.EventListener eventListener) { - if (channel.getType() == Channel.TYPE_FILE) { - // MPEG2 TS captured stream file recording is not supported. - if (mIsRecording) { - return null; - } - FileTsStreamer streamer = new FileTsStreamer(eventListener, context); - if (streamer.startStream(channel)) { - TsDataSource source = streamer.createDataSource(); - sTsStreamers.put(source, streamer); - return source; - } - return null; - } - return mTunerStreamerManager.createDataSource(context, channel, eventListener, - mId, !mIsRecording && mKeepTuneStatus); - } - - /** - * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}. - * @param source to release - */ - public void releaseDataSource(TsDataSource source) { - if (source instanceof TunerTsStreamer.TunerDataSource) { - mTunerStreamerManager.releaseDataSource( - source, mId, !mIsRecording && mKeepTuneStatus); - } else if (source instanceof FileTsStreamer.FileDataSource) { - FileTsStreamer streamer = (FileTsStreamer) sTsStreamers.get(source); - if (streamer != null) { - sTsStreamers.remove(source); - streamer.stopStream(); - } - } - } - - /** - * Indicates that the current session has pending tunes. - */ - public void setHasPendingTune() { - mTunerStreamerManager.setHasPendingTune(mId); - } - - /** - * Indicates whether the underlying {@link TunerHal} 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. - */ - public void setKeepTuneStatus(boolean keepTuneStatus) { - mKeepTuneStatus = keepTuneStatus; - } - - /** - * Add tuner hal into TunerTsStreamerManager for test. - */ - @VisibleForTesting - public void addTunerHalForTest(TunerHal tunerHal) { - mTunerStreamerManager.addTunerHal(tunerHal, mId); - } - - /** - * Releases persistent resources. - */ - public void release() { - mTunerStreamerManager.release(mId); - } -} diff --git a/src/com/android/tv/tuner/source/TsStreamWriter.java b/src/com/android/tv/tuner/source/TsStreamWriter.java deleted file mode 100644 index 30650555..00000000 --- a/src/com/android/tv/tuner/source/TsStreamWriter.java +++ /dev/null @@ -1,237 +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.source; - -import android.content.Context; -import android.util.Log; -import com.android.tv.tuner.data.TunerChannel; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -/** - * Stores TS files to the disk for debugging. - */ -public class TsStreamWriter { - private static final String TAG = "TsStreamWriter"; - private static final boolean DEBUG = false; - - private static final long TIME_LIMIT_MS = 10000; // 10s - private static final int NO_INSTANCE_ID = 0; - private static final int MAX_GET_ID_RETRY_COUNT = 5; - private static final int MAX_INSTANCE_ID = 10000; - private static final String SEPARATOR = "_"; - - private FileOutputStream mFileOutputStream; - private long mFileStartTimeMs; - private String mFileName = null; - private final String mDirectoryPath; - private final File mDirectory; - private final int mInstanceId; - private TunerChannel mChannel; - - public TsStreamWriter(Context context) { - File externalFilesDir = context.getExternalFilesDir(null); - if (externalFilesDir == null || !externalFilesDir.isDirectory()) { - mDirectoryPath = null; - mDirectory = null; - mInstanceId = NO_INSTANCE_ID; - if (DEBUG) { - Log.w(TAG, "Fail to get external files dir!"); - } - } else { - mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream"; - mDirectory = new File(mDirectoryPath); - if (!mDirectory.exists()) { - boolean madeDir = mDirectory.mkdir(); - if (!madeDir) { - Log.w(TAG, "Error. Fail to create folder!"); - } - } - mInstanceId = generateInstanceId(); - } - } - - /** - * Sets the current channel. - * - * @param channel curren channel of the stream - */ - public void setChannel(TunerChannel channel) { - mChannel = channel; - } - - /** - * Opens a file to store TS data. - */ - public void openFile() { - if (mChannel == null || mDirectoryPath == null) { - return; - } - mFileStartTimeMs = System.currentTimeMillis(); - mFileName = mChannel.getDisplayNumber() + SEPARATOR + mFileStartTimeMs + SEPARATOR - + mInstanceId + ".ts"; - String filePath = mDirectoryPath + "/" + mFileName; - try { - mFileOutputStream = new FileOutputStream(filePath, false); - } catch (FileNotFoundException e) { - Log.w(TAG, "Cannot open file: " + filePath, e); - } - } - - /** - * Closes the file and stops storing TS data. - * - * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped - * {@code false} otherwise - */ - public void closeFile(boolean calledWhenStopStream) { - if (mFileOutputStream == null) { - return; - } - try { - mFileOutputStream.close(); - deleteOutdatedFiles(calledWhenStopStream); - mFileName = null; - mFileOutputStream = null; - } catch (IOException e) { - Log.w(TAG, "Error on closing file.", e); - } - } - - /** - * Writes the data to the file. - * - * @param buffer the data to be written - * @param bytesWritten number of bytes written - */ - public void writeToFile(byte[] buffer, int bytesWritten) { - if (mFileOutputStream == null) { - return; - } - if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) { - closeFile(false); - openFile(); - } - try { - mFileOutputStream.write(buffer, 0, bytesWritten); - } catch (IOException e) { - Log.w(TAG, "Error on writing TS stream.", e); - } - } - - /** - * Deletes outdated files to save storage. - * - * @param deleteAll {@code true} if all the files with the relative ID should be deleted - * {@code false} if the most recent file should not be deleted - */ - private void deleteOutdatedFiles(boolean deleteAll) { - if (mFileName == null) { - return; - } - if (mDirectory == null || !mDirectory.isDirectory()) { - Log.e(TAG, "Error. The folder doesn't exist!"); - return; - } - if (mFileName == null) { - Log.e(TAG, "Error. The current file name is null!"); - return; - } - for (File file : mDirectory.listFiles()) { - if (file.isFile() && getFileId(file) == mInstanceId - && (deleteAll || !mFileName.equals(file.getName()))) { - boolean deleted = file.delete(); - if (DEBUG && !deleted) { - Log.w(TAG, "Failed to delete " + file.getName()); - } - } - } - } - - /** - * Generates a unique instance ID. - * - * @return a unique instance ID - */ - private int generateInstanceId() { - if (mDirectory == null) { - return NO_INSTANCE_ID; - } - Set<Integer> idSet = getExistingIds(); - if (idSet == null) { - return NO_INSTANCE_ID; - } - for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) { - // Range [1, MAX_INSTANCE_ID] - int id = (int)Math.floor(Math.random() * MAX_INSTANCE_ID) + 1; - if (!idSet.contains(id)) { - return id; - } - } - return NO_INSTANCE_ID; - } - - /** - * Gets all existing instance IDs. - * - * @return a set of all existing instance IDs - */ - private Set<Integer> getExistingIds() { - if (mDirectory == null || !mDirectory.isDirectory()) { - return null; - } - - Set<Integer> idSet = new HashSet<>(); - for (File file : mDirectory.listFiles()) { - int id = getFileId(file); - if(id != NO_INSTANCE_ID) { - idSet.add(id); - } - } - return idSet; - } - - /** - * Gets the instance ID of a given file. - * - * @param file the file whose TsStreamWriter ID is returned - * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available - */ - private static int getFileId(File file) { - if (file == null || !file.isFile()) { - return NO_INSTANCE_ID; - } - String fileName = file.getName(); - int lastSeparator = fileName.lastIndexOf(SEPARATOR); - if (!fileName.endsWith(".ts") || lastSeparator == -1) { - return NO_INSTANCE_ID; - } - try { - return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3)); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.e(TAG, fileName + " is not a valid file name."); - } - } - return NO_INSTANCE_ID; - } -} diff --git a/src/com/android/tv/tuner/source/TsStreamer.java b/src/com/android/tv/tuner/source/TsStreamer.java deleted file mode 100644 index 1ac950bb..00000000 --- a/src/com/android/tv/tuner/source/TsStreamer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.data.TunerChannel; - -/** - * Interface definition for a stream generator. The interface will provide streams - * for scanning channels and/or playback. - */ -public interface TsStreamer { - /** - * Starts streaming the data for channel scanning process. - * - * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned - * @return {@code true} if ready to stream, otherwise {@code false} - */ - boolean startStream(ChannelScanFileParser.ScanChannel channel); - - /** - * Starts streaming the data for channel playing or recording. - * - * @param channel {@link TunerChannel} to tune - * @return {@code true} if ready to stream, otherwise {@code false} - */ - boolean startStream(TunerChannel channel); - - /** - * Stops streaming the data. - */ - void stopStream(); - - /** - * Creates {@link TsDataSource} which will provide MPEG-2 TS stream for - * {@link android.media.MediaExtractor}. The source will start from the position - * where it is created. - * - * @return {@link TsDataSource} - */ - TsDataSource createDataSource(); -} diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java deleted file mode 100644 index 843cbdb7..00000000 --- a/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import android.content.Context; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSpec; -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.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Provides MPEG-2 TS stream sources for channel playing from an underlying tuner device. - */ -public class TunerTsStreamer implements TsStreamer { - private static final String TAG = "TunerTsStreamer"; - - private static final int MIN_READ_UNIT = 1500; - private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB - private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB - private static final int TS_PACKET_SIZE = 188; - - private static final int READ_TIMEOUT_MS = 5000; // 5 secs. - private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; - private static final int READ_ERROR_STREAMING_ENDED = -1; - private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2; - - private final Object mCircularBufferMonitor = new Object(); - private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; - private long mBytesFetched; - private final AtomicLong mLastReadPosition = new AtomicLong(); - private boolean mStreaming; - - private final TunerHal mTunerHal; - private TunerChannel mChannel; - private Thread mStreamingThread; - private final EventDetector mEventDetector; - private final List<Pair<EventListener, Boolean>> mEventListenerActions = new ArrayList<>(); - - private final TsStreamWriter mTsStreamWriter; - private String mChannelNumber; - - public static class TunerDataSource extends TsDataSource { - private final TunerTsStreamer mTsStreamer; - private final AtomicLong mLastReadPosition = new AtomicLong(0); - private long mStartBufferedPosition; - - private TunerDataSource(TunerTsStreamer tsStreamer) { - mTsStreamer = tsStreamer; - mStartBufferedPosition = tsStreamer.getBufferedPosition(); - } - - @Override - public long getBufferedPosition() { - return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; - } - - @Override - public long getLastReadPosition() { - return mLastReadPosition.get(); - } - - @Override - public void shiftStartPosition(long offset) { - SoftPreconditions.checkState(mLastReadPosition.get() == 0); - SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition()); - mStartBufferedPosition += offset; - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - mLastReadPosition.set(0); - return C.LENGTH_UNBOUNDED; - } - - @Override - public void close() { - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, - offset, readLength); - if (ret > 0) { - mLastReadPosition.addAndGet(ret); - } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) { - long currentPosition = mStartBufferedPosition + mLastReadPosition.get(); - long endPosition = mTsStreamer.getBufferedPosition(); - long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE) - * TS_PACKET_SIZE; - Log.w(TAG, "Demux position jump by overwritten buffer: " + diff); - mStartBufferedPosition = currentPosition + diff; - mLastReadPosition.set(0); - return 0; - } - return ret; - } - } - /** - * Creates {@link TsStreamer} for playing or recording the specified channel. - * @param tunerHal the HAL for tuner device - * @param eventListener the listener for channel & program information - */ - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { - mTunerHal = tunerHal; - mEventDetector = new EventDetector(mTunerHal); - if (eventListener != null) { - mEventDetector.registerListener(eventListener); - } - mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? - new TsStreamWriter(context) : null; - } - - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { - this(tunerHal, eventListener, null); - } - - @Override - public boolean startStream(TunerChannel channel) { - if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(), - channel.getDisplayNumber(false))) { - if (channel.hasVideo()) { - mTunerHal.addPidFilter(channel.getVideoPid(), - TunerHal.FILTER_TYPE_VIDEO); - } - boolean audioFilterSet = false; - for (Integer audioPid : channel.getAudioPids()) { - if (!audioFilterSet) { - mTunerHal.addPidFilter(audioPid, TunerHal.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(channel.getPcrPid(), - TunerHal.FILTER_TYPE_PCR); - if (mEventDetector != null) { - mEventDetector.startDetecting(channel.getFrequency(), channel.getModulation(), - channel.getProgramNumber()); - } - mChannel = channel; - mChannelNumber = channel.getDisplayNumber(); - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - Log.w(TAG, "Streaming should be stopped before start streaming"); - return true; - } - mStreaming = true; - mBytesFetched = 0; - mLastReadPosition.set(0L); - } - if (mTsStreamWriter != null) { - mTsStreamWriter.setChannel(mChannel); - mTsStreamWriter.openFile(); - } - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - return false; - } - - @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { - mEventDetector.startDetecting( - channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - Log.w(TAG, "Streaming should be stopped before start streaming"); - return true; - } - mStreaming = true; - mBytesFetched = 0; - mLastReadPosition.set(0L); - } - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - return false; - } - - /** - * Blocks the current thread until the streaming thread stops. In rare cases when the tuner - * device is overloaded this can take a while, but usually it returns pretty quickly. - */ - @Override - public void stopStream() { - mChannel = null; - synchronized (mCircularBufferMonitor) { - mStreaming = false; - mCircularBufferMonitor.notifyAll(); - } - - try { - if (mStreamingThread != null) { - mStreamingThread.join(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - if (mTsStreamWriter != null) { - mTsStreamWriter.closeFile(true); - mTsStreamWriter.setChannel(null); - } - } - - @Override - public TsDataSource createDataSource() { - return new TunerDataSource(this); - } - - /** - * Returns incomplete channel lists which was scanned so far. Incomplete channel means - * the channel whose channel information is not complete or is not well-formed. - * @return {@link List} of {@link TunerChannel} - */ - public List<TunerChannel> getMalFormedChannels() { - return mEventDetector.getMalFormedChannels(); - } - - /** - * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer. - * @return {@link TunerHal} - */ - public TunerHal getTunerHal() { - return mTunerHal; - } - - /** - * Returns the current tuned channel for TunerTsStreamer. - * @return {@link TunerChannel} - */ - public TunerChannel getChannel() { - return mChannel; - } - - /** - * Returns the current buffered position from tuner. - * @return the current buffered position - */ - public long getBufferedPosition() { - synchronized (mCircularBufferMonitor) { - return mBytesFetched; - } - } - - public String getStreamerInfo() { - return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming; - } - - public void registerListener(EventListener listener) { - if (mEventDetector != null && listener != null) { - synchronized (mEventListenerActions) { - mEventListenerActions.add(new Pair<>(listener, true)); - } - } - } - - public void unregisterListener(EventListener listener) { - if (mEventDetector != null) { - synchronized (mEventListenerActions) { - mEventListenerActions.add(new Pair(listener, false)); - } - } - } - - private class StreamingThread extends Thread { - @Override - public void run() { - // Buffers for streaming data from the tuner and the internal buffer. - byte[] dataBuffer = new byte[READ_BUFFER_SIZE]; - - while (true) { - synchronized (mCircularBufferMonitor) { - if (!mStreaming) { - break; - } - } - - if (mEventDetector != null) { - synchronized (mEventListenerActions) { - for (Pair listenerAction : mEventListenerActions) { - EventListener listener = (EventListener) listenerAction.first; - if ((boolean) listenerAction.second) { - mEventDetector.registerListener(listener); - } else { - mEventDetector.unregisterListener(listener); - } - } - mEventListenerActions.clear(); - } - } - - int bytesWritten = mTunerHal.readTsStream(dataBuffer, dataBuffer.length); - if (bytesWritten <= 0) { - try { - // When buffer is underrun, we sleep for short time to prevent - // unnecessary CPU draining. - sleep(BUFFER_UNDERRUN_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - continue; - } - - if (mTsStreamWriter != null) { - mTsStreamWriter.writeToFile(dataBuffer, bytesWritten); - } - - if (mEventDetector != null) { - mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); - } - synchronized (mCircularBufferMonitor) { - int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = bytesWritten; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(dataBuffer, 0, mCircularBuffer, posInBuffer, - bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < bytesWritten) { - System.arraycopy(dataBuffer, bytesToCopyInFirstPass, mCircularBuffer, 0, - bytesWritten - bytesToCopyInFirstPass); - } - mBytesFetched += bytesWritten; - mCircularBufferMonitor.notifyAll(); - } - } - - Log.i(TAG, "Streaming stopped"); - } - } - - /** - * Reads data from internal buffer. - * @param pos the position to read from - * @param buffer to read - * @param offset start position of the read buffer - * @param amount number of bytes to read - * @return number of read bytes when successful, {@code -1} otherwise - * @throws IOException - */ - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - while (true) { - synchronized (mCircularBufferMonitor) { - if (!mStreaming) { - return READ_ERROR_STREAMING_ENDED; - } - if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.w(TAG, "Demux is requesting the data which is already overwritten."); - return READ_ERROR_BUFFER_OVERWRITTEN; - } - if (mBytesFetched < pos + amount) { - try { - mCircularBufferMonitor.wait(READ_TIMEOUT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - // Try again to prevent starvation. - // Give chances to read from other threads. - continue; - } - int startPos = (int) (pos % CIRCULAR_BUFFER_SIZE); - int endPos = (int) ((pos + amount) % CIRCULAR_BUFFER_SIZE); - int firstLength = (startPos > endPos ? CIRCULAR_BUFFER_SIZE : endPos) - startPos; - System.arraycopy(mCircularBuffer, startPos, buffer, offset, firstLength); - if (firstLength < amount) { - System.arraycopy(mCircularBuffer, 0, buffer, offset + firstLength, - amount - firstLength); - } - mCircularBufferMonitor.notifyAll(); - return amount; - } - } - } -} diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java deleted file mode 100644 index 258a4d86..00000000 --- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ /dev/null @@ -1,304 +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.source; - -import android.content.Context; - -import com.android.tv.common.AutoCloseableUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * 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 class directly. - */ -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<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); - private final TunerHalManager mTunerHalManager = new TunerHalManager(); - private static TunerTsStreamerManager sInstance; - - /** - * Returns the singleton instance for the class - * @return TunerTsStreamerManager - */ - static synchronized TunerTsStreamerManager getInstance() { - if (sInstance == null) { - sInstance = new TunerTsStreamerManager(); - } - return sInstance; - } - - private TunerTsStreamerManager() { } - - synchronized TsDataSource createDataSource( - Context context, TunerChannel channel, EventDetector.EventListener listener, - int sessionId, boolean reuse) { - TsStreamerCreator creator; - synchronized (mCancelLock) { - if (mStreamerFinder.containsLocked(channel)) { - mStreamerFinder.appendSessionLocked(channel, sessionId); - TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel); - TsDataSource source = streamer.createDataSource(); - mListeners.put(sessionId, listener); - streamer.registerListener(listener); - mSourceToStreamerMap.put(source, streamer); - return source; - } - creator = new TsStreamerCreator(context, channel, listener); - mCreators.put(sessionId, creator); - } - TunerTsStreamer streamer = creator.create(sessionId, reuse); - synchronized (mCancelLock) { - mCreators.remove(sessionId); - if (streamer == null) { - return null; - } - if (!creator.isCancelledLocked()) { - mStreamerFinder.putLocked(channel, sessionId, streamer); - TsDataSource source = streamer.createDataSource(); - mListeners.put(sessionId, listener); - mSourceToStreamerMap.put(source, streamer); - return source; - } - } - // Created streamer was cancelled by a new tune request. - streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); - hal.setHasPendingTune(false); - mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); - return null; - } - - synchronized void releaseDataSource(TsDataSource source, int sessionId, - boolean reuse) { - TunerTsStreamer streamer; - synchronized (mCancelLock) { - streamer = mSourceToStreamerMap.get(source); - mSourceToStreamerMap.remove(source); - if (streamer == null) { - return; - } - EventDetector.EventListener listener = mListeners.remove(sessionId); - streamer.unregisterListener(listener); - TunerChannel channel = streamer.getChannel(); - SoftPreconditions.checkState(channel != null); - mStreamerFinder.removeSessionLocked(channel, sessionId); - if (mStreamerFinder.containsLocked(channel)) { - return; - } - } - streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); - hal.setHasPendingTune(false); - mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); - } - - void setHasPendingTune(int sessionId) { - synchronized (mCancelLock) { - if (mCreators.containsKey(sessionId)) { - mCreators.get(sessionId).cancelLocked(); - } - } - } - - /** - * Add tuner hal into TunerHalManager for test. - */ - void addTunerHal(TunerHal tunerHal, int sessionId) { - mTunerHalManager.addTunerHal(tunerHal, sessionId); - } - - synchronized void release(int sessionId) { - mTunerHalManager.releaseCachedHal(sessionId); - } - - private class StreamerFinder { - private final Map<TunerChannel, Set<Integer>> mSessions = new HashMap<>(); - private final Map<TunerChannel, TunerTsStreamer> mStreamers = new HashMap<>(); - - // @GuardedBy("mCancelLock") - private void putLocked(TunerChannel channel, int sessionId, TunerTsStreamer streamer) { - Set<Integer> sessions = new HashSet<>(); - sessions.add(sessionId); - mSessions.put(channel, sessions); - mStreamers.put(channel, streamer); - } - - // @GuardedBy("mCancelLock") - private void appendSessionLocked(TunerChannel channel, int sessionId) { - if (mSessions.containsKey(channel)) { - mSessions.get(channel).add(sessionId); - } - } - - // @GuardedBy("mCancelLock") - private void removeSessionLocked(TunerChannel channel, int sessionId) { - Set<Integer> sessions = mSessions.get(channel); - sessions.remove(sessionId); - if (sessions.size() == 0) { - mSessions.remove(channel); - mStreamers.remove(channel); - } - } - - // @GuardedBy("mCancelLock") - private boolean containsLocked(TunerChannel channel) { - return mSessions.containsKey(channel); - } - - // @GuardedBy("mCancelLock") - private TunerTsStreamer getStreamerLocked(TunerChannel channel) { - return mStreamers.containsKey(channel) ? mStreamers.get(channel) : null; - } - } - - /** - * {@link TunerTsStreamer} creation can be cancelled by a new tune request for the same - * session. The class supports the cancellation in creating new {@link TunerTsStreamer}. - */ - private class TsStreamerCreator { - private final Context mContext; - private final TunerChannel mChannel; - private final EventDetector.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 TsStreamerCreator(Context context, TunerChannel channel, - EventDetector.EventListener listener) { - mContext = context; - mChannel = channel; - mEventListener = listener; - } - - private TunerTsStreamer create(int sessionId, boolean reuse) { - TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId); - if (hal == null) { - return null; - } - boolean canceled = false; - synchronized (mCancelLock) { - if (!mCancelled) { - mTunerHal = hal; - } else { - canceled = true; - } - } - if (!canceled) { - TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext); - if (tsStreamer.startStream(mChannel)) { - return tsStreamer; - } - synchronized (mCancelLock) { - mTunerHal = null; - } - } - hal.setHasPendingTune(false); - // Since TunerTsStreamer is not properly created, closes TunerHal. - // And do not re-use TunerHal when it is not cancelled. - mTunerHalManager.releaseTunerHal(hal, sessionId, mCancelled && reuse); - return null; - } - - // @GuardedBy("mCancelLock") - private void cancelLocked() { - if (mCancelled) { - return; - } - mCancelled = true; - if (mTunerHal != null) { - mTunerHal.setHasPendingTune(true); - } - } - - // @GuardedBy("mCancelLock") - private boolean isCancelledLocked() { - return mCancelled; - } - } - - /** - * Supports sharing {@link TunerHal} among multiple sessions. - * The class also supports session affinity for {@link TunerHal} allocation. - */ - private class TunerHalManager { - private final Map<Integer, TunerHal> mTunerHals = new HashMap<>(); - - private TunerHal getOrCreateTunerHal(Context context, int sessionId) { - // Handles session affinity. - TunerHal hal = mTunerHals.get(sessionId); - if (hal != null) { - mTunerHals.remove(sessionId); - return hal; - } - // Finds a TunerHal which is cached for other sessions. - Iterator it = mTunerHals.keySet().iterator(); - if (it.hasNext()) { - Integer key = (Integer) it.next(); - hal = mTunerHals.get(key); - mTunerHals.remove(key); - return hal; - } - return TunerHal.createInstance(context); - } - - private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { - if (!reuse || !hal.isReusable()) { - AutoCloseableUtils.closeQuietly(hal); - return; - } - TunerHal cachedHal = mTunerHals.get(sessionId); - if (cachedHal != hal) { - mTunerHals.put(sessionId, hal); - if (cachedHal != null) { - AutoCloseableUtils.closeQuietly(cachedHal); - } - } - } - - private void releaseCachedHal(int sessionId) { - TunerHal hal = mTunerHals.get(sessionId); - if (hal != null) { - mTunerHals.remove(sessionId); - } - if (hal != null) { - AutoCloseableUtils.closeQuietly(hal); - } - } - - private void addTunerHal(TunerHal tunerHal, int sessionId) { - mTunerHals.put(sessionId, tunerHal); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java deleted file mode 100644 index e1f890f3..00000000 --- a/src/com/android/tv/tuner/ts/SectionParser.java +++ /dev/null @@ -1,1759 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.ts; - -import android.media.tv.TvContentRating; -import android.media.tv.TvContract.Programs.Genres; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Log; -import android.util.SparseArray; - -import com.android.tv.tuner.data.PsiData.PatItem; -import com.android.tv.tuner.data.PsiData.PmtItem; -import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor; -import com.android.tv.tuner.data.PsipData.CaptionServiceDescriptor; -import com.android.tv.tuner.data.PsipData.ContentAdvisoryDescriptor; -import com.android.tv.tuner.data.PsipData.EitItem; -import com.android.tv.tuner.data.PsipData.EttItem; -import com.android.tv.tuner.data.PsipData.ExtendedChannelNameDescriptor; -import com.android.tv.tuner.data.PsipData.GenreDescriptor; -import com.android.tv.tuner.data.PsipData.Iso639LanguageDescriptor; -import com.android.tv.tuner.data.PsipData.MgtItem; -import com.android.tv.tuner.data.PsipData.ParentalRatingDescriptor; -import com.android.tv.tuner.data.PsipData.PsipSection; -import com.android.tv.tuner.data.PsipData.RatingRegion; -import com.android.tv.tuner.data.PsipData.RegionalRating; -import com.android.tv.tuner.data.PsipData.SdtItem; -import com.android.tv.tuner.data.PsipData.ServiceDescriptor; -import com.android.tv.tuner.data.PsipData.ShortEventDescriptor; -import com.android.tv.tuner.data.PsipData.TsDescriptor; -import com.android.tv.tuner.data.PsipData.VctItem; -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.util.ByteArrayBuffer; - -import com.android.tv.tuner.util.ConvertUtils; -import com.ibm.icu.text.UnicodeDecompressor; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.Calendar; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Parses ATSC PSIP sections. - */ -public class SectionParser { - private static final String TAG = "SectionParser"; - private static final boolean DEBUG = false; - - private static final byte TABLE_ID_PAT = (byte) 0x00; - private static final byte TABLE_ID_PMT = (byte) 0x02; - private static final byte TABLE_ID_MGT = (byte) 0xc7; - private static final byte TABLE_ID_TVCT = (byte) 0xc8; - private static final byte TABLE_ID_CVCT = (byte) 0xc9; - private static final byte TABLE_ID_EIT = (byte) 0xcb; - private static final byte TABLE_ID_ETT = (byte) 0xcc; - - // Table id for DVB - private static final byte TABLE_ID_SDT = (byte) 0x42; - private static final byte TABLE_ID_DVB_ACTUAL_P_F_EIT = (byte) 0x4e; - private static final byte TABLE_ID_DVB_OTHER_P_F_EIT = (byte) 0x4f; - private static final byte TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT = (byte) 0x50; - private static final byte TABLE_ID_DVB_OTHER_SCHEDULE_EIT = (byte) 0x60; - - // For details of the structure for the tags of descriptors, see ATSC A/65 Table 6.25. - public static final int DESCRIPTOR_TAG_ISO639LANGUAGE = 0x0a; - public static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; - public static final int DESCRIPTOR_TAG_CONTENT_ADVISORY = 0x87; - public static final int DESCRIPTOR_TAG_AC3_AUDIO_STREAM = 0x81; - public static final int DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = 0xa0; - public static final int DESCRIPTOR_TAG_GENRE = 0xab; - - // For details of the structure for the tags of DVB descriptors, see DVB Document A038 Table 12. - public static final int DVB_DESCRIPTOR_TAG_SERVICE = 0x48; - public static final int DVB_DESCRIPTOR_TAG_SHORT_EVENT = 0X4d; - public static final int DVB_DESCRIPTOR_TAG_CONTENT = 0x54; - public static final int DVB_DESCRIPTOR_TAG_PARENTAL_RATING = 0x55; - - private static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00; - private static final byte MODE_SELECTED_UNICODE_RANGE_1 = (byte) 0x00; // 0x0000 - 0x00ff - private static final byte MODE_UTF16 = (byte) 0x3f; - private static final byte MODE_SCSU = (byte) 0x3e; - private static final int MAX_SHORT_NAME_BYTES = 14; - - // See ANSI/CEA-766-C. - private static final int RATING_REGION_US_TV = 1; - private static final int RATING_REGION_KR_TV = 4; - - // The following values are defined in the live channels app. - // See https://developer.android.com/reference/android/media/tv/TvContentRating.html. - private static final String RATING_DOMAIN = "com.android.tv"; - private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV"; - private static final String RATING_REGION_RATING_SYSTEM_US_MV = "US_MV"; - private static final String RATING_REGION_RATING_SYSTEM_KR_TV = "KR_TV"; - - private static final String[] RATING_REGION_TABLE_US_TV = { - "US_TV_Y", "US_TV_Y7", "US_TV_G", "US_TV_PG", "US_TV_14", "US_TV_MA" - }; - - private static final String[] RATING_REGION_TABLE_US_MV = { - "US_MV_G", "US_MV_PG", "US_MV_PG13", "US_MV_R", "US_MV_NC17" - }; - - private static final String[] RATING_REGION_TABLE_KR_TV = { - "KR_TV_ALL", "KR_TV_7", "KR_TV_12", "KR_TV_15", "KR_TV_19" - }; - - private static final String[] RATING_REGION_TABLE_US_TV_SUBRATING = { - "US_TV_D", "US_TV_L", "US_TV_S", "US_TV_V", "US_TV_FV" - }; - - // According to ANSI-CEA-766-D - private static final int VALUE_US_TV_Y = 1; - private static final int VALUE_US_TV_Y7 = 2; - private static final int VALUE_US_TV_NONE = 1; - private static final int VALUE_US_TV_G = 2; - private static final int VALUE_US_TV_PG = 3; - private static final int VALUE_US_TV_14 = 4; - private static final int VALUE_US_TV_MA = 5; - - private static final int DIMENSION_US_TV_RATING = 0; - private static final int DIMENSION_US_TV_D = 1; - private static final int DIMENSION_US_TV_L = 2; - private static final int DIMENSION_US_TV_S = 3; - private static final int DIMENSION_US_TV_V = 4; - private static final int DIMENSION_US_TV_Y = 5; - private static final int DIMENSION_US_TV_FV = 6; - private static final int DIMENSION_US_MV_RATING = 7; - - private static final int VALUE_US_MV_G = 2; - private static final int VALUE_US_MV_PG = 3; - private static final int VALUE_US_MV_PG13 = 4; - private static final int VALUE_US_MV_R = 5; - private static final int VALUE_US_MV_NC17 = 6; - private static final int VALUE_US_MV_X = 7; - - private static final String STRING_US_TV_Y = "US_TV_Y"; - private static final String STRING_US_TV_Y7 = "US_TV_Y7"; - private static final String STRING_US_TV_FV = "US_TV_FV"; - - - /* - * The following CRC table is from the code generated by the following command. - * $ python pycrc.py --model crc-32-mpeg --algorithm table-driven --generate c - * To see the details of pycrc, visit http://www.tty1.net/pycrc/index_en.html - */ - public static final int[] CRC_TABLE = { - 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, - 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, - 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, - 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, - 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, - 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, - 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, - 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, - 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, - 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, - 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, - 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, - 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, - 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, - 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, - 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, - 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, - 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, - 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, - 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, - 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, - 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, - 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, - 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, - 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, - 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, - 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, - 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, - 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, - 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, - 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, - 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, - 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, - 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, - 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, - 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, - 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, - 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, - 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, - 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, - 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, - 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, - 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, - 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, - 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, - 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, - 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, - 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, - 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, - 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, - 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, - 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, - 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, - 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, - 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, - 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, - 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, - 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, - 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, - 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, - 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, - 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, - 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, - 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 - }; - - // A table which maps ATSC genres to TIF genres. - // See ATSC/65 Table 6.20. - private static final String[] CANONICAL_GENRES_TABLE = { - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - Genres.EDUCATION, Genres.ENTERTAINMENT, Genres.MOVIES, Genres.NEWS, - Genres.LIFE_STYLE, Genres.SPORTS, null, Genres.MOVIES, - null, - Genres.FAMILY_KIDS, Genres.DRAMA, null, Genres.ENTERTAINMENT, Genres.SPORTS, - Genres.SPORTS, - null, null, - Genres.MUSIC, Genres.EDUCATION, - null, - Genres.COMEDY, - null, - Genres.MUSIC, - null, null, - Genres.MOVIES, Genres.ENTERTAINMENT, Genres.NEWS, Genres.DRAMA, - Genres.EDUCATION, Genres.MOVIES, Genres.SPORTS, Genres.MOVIES, - null, - Genres.LIFE_STYLE, Genres.ARTS, Genres.LIFE_STYLE, Genres.SPORTS, - null, null, - Genres.GAMING, Genres.LIFE_STYLE, Genres.SPORTS, - null, - Genres.LIFE_STYLE, Genres.EDUCATION, Genres.EDUCATION, Genres.LIFE_STYLE, - Genres.SPORTS, Genres.LIFE_STYLE, Genres.MOVIES, Genres.NEWS, - null, null, null, - Genres.EDUCATION, - null, null, null, - Genres.EDUCATION, - null, null, null, - Genres.DRAMA, Genres.MUSIC, Genres.MOVIES, - null, - Genres.ANIMAL_WILDLIFE, - null, null, - Genres.PREMIER, - null, null, null, null, - Genres.SPORTS, Genres.ARTS, - null, null, null, - Genres.MOVIES, Genres.TECH_SCIENCE, Genres.DRAMA, - null, - Genres.SHOPPING, Genres.DRAMA, - null, - Genres.MOVIES, Genres.ENTERTAINMENT, Genres.TECH_SCIENCE, Genres.SPORTS, - Genres.TRAVEL, Genres.ENTERTAINMENT, Genres.ARTS, Genres.NEWS, - null, - Genres.ARTS, Genres.SPORTS, Genres.SPORTS, Genres.NEWS, - Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, Genres.FAMILY_KIDS, - Genres.FAMILY_KIDS, Genres.MOVIES, - null, - Genres.TECH_SCIENCE, Genres.MUSIC, - null, - Genres.SPORTS, Genres.FAMILY_KIDS, Genres.NEWS, Genres.SPORTS, - Genres.NEWS, Genres.SPORTS, Genres.ANIMAL_WILDLIFE, - null, - Genres.MUSIC, Genres.NEWS, Genres.SPORTS, - null, - Genres.NEWS, Genres.NEWS, Genres.NEWS, Genres.NEWS, - Genres.SPORTS, Genres.MOVIES, Genres.ARTS, Genres.ANIMAL_WILDLIFE, - Genres.MUSIC, Genres.MUSIC, Genres.MOVIES, Genres.EDUCATION, - Genres.DRAMA, Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, - Genres.SPORTS, - null, - Genres.SPORTS, Genres.SPORTS, - }; - - // A table which contains ATSC categorical genre code assignments. - // See ATSC/65 Table 6.20. - private static final String[] BROADCAST_GENRES_TABLE = new String[] { - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - "Education", "Entertainment", "Movie", "News", - "Religious", "Sports", "Other", "Action", - "Advertisement", "Animated", "Anthology", "Automobile", - "Awards", "Baseball", "Basketball", "Bulletin", - "Business", "Classical", "College", "Combat", - "Comedy", "Commentary", "Concert", "Consumer", - "Contemporary", "Crime", "Dance", "Documentary", - "Drama", "Elementary", "Erotica", "Exercise", - "Fantasy", "Farm", "Fashion", "Fiction", - "Food", "Football", "Foreign", "Fund Raiser", - "Game/Quiz", "Garden", "Golf", "Government", - "Health", "High School", "History", "Hobby", - "Hockey", "Home", "Horror", "Information", - "Instruction", "International", "Interview", "Language", - "Legal", "Live", "Local", "Math", - "Medical", "Meeting", "Military", "Miniseries", - "Music", "Mystery", "National", "Nature", - "Police", "Politics", "Premier", "Prerecorded", - "Product", "Professional", "Public", "Racing", - "Reading", "Repair", "Repeat", "Review", - "Romance", "Science", "Series", "Service", - "Shopping", "Soap Opera", "Special", "Suspense", - "Talk", "Technical", "Tennis", "Travel", - "Variety", "Video", "Weather", "Western", - "Art", "Auto Racing", "Aviation", "Biography", - "Boating", "Bowling", "Boxing", "Cartoon", - "Children", "Classic Film", "Community", "Computers", - "Country Music", "Court", "Extreme Sports", "Family", - "Financial", "Gymnastics", "Headlines", "Horse Racing", - "Hunting/Fishing/Outdoors", "Independent", "Jazz", "Magazine", - "Motorcycle Racing", "Music/Film/Books", "News-International", "News-Local", - "News-National", "News-Regional", "Olympics", "Original", - "Performing Arts", "Pets/Animals", "Pop", "Rock & Roll", - "Sci-Fi", "Self Improvement", "Sitcom", "Skating", - "Skiing", "Soccer", "Track/Field", "True", - "Volleyball", "Wrestling", - }; - - // Audio language code map from ISO 639-2/B to 639-2/T, in order to show correct audio language. - private static final HashMap<String, String> ISO_LANGUAGE_CODE_MAP; - static { - ISO_LANGUAGE_CODE_MAP = new HashMap<>(); - ISO_LANGUAGE_CODE_MAP.put("alb", "sqi"); - ISO_LANGUAGE_CODE_MAP.put("arm", "hye"); - ISO_LANGUAGE_CODE_MAP.put("baq", "eus"); - ISO_LANGUAGE_CODE_MAP.put("bur", "mya"); - ISO_LANGUAGE_CODE_MAP.put("chi", "zho"); - ISO_LANGUAGE_CODE_MAP.put("cze", "ces"); - ISO_LANGUAGE_CODE_MAP.put("dut", "nld"); - ISO_LANGUAGE_CODE_MAP.put("fre", "fra"); - ISO_LANGUAGE_CODE_MAP.put("geo", "kat"); - ISO_LANGUAGE_CODE_MAP.put("ger", "deu"); - ISO_LANGUAGE_CODE_MAP.put("gre", "ell"); - ISO_LANGUAGE_CODE_MAP.put("ice", "isl"); - ISO_LANGUAGE_CODE_MAP.put("mac", "mkd"); - ISO_LANGUAGE_CODE_MAP.put("mao", "mri"); - ISO_LANGUAGE_CODE_MAP.put("may", "msa"); - ISO_LANGUAGE_CODE_MAP.put("per", "fas"); - ISO_LANGUAGE_CODE_MAP.put("rum", "ron"); - ISO_LANGUAGE_CODE_MAP.put("slo", "slk"); - ISO_LANGUAGE_CODE_MAP.put("tib", "bod"); - ISO_LANGUAGE_CODE_MAP.put("wel", "cym"); - ISO_LANGUAGE_CODE_MAP.put("esl", "spa"); // Special entry for channel 9-1 KQED in bay area. - } - - // Containers to store the last version numbers of the PSIP sections. - private final HashMap<PsipSection, Integer> mSectionVersionMap = new HashMap<>(); - private final SparseArray<List<EttItem>> mParsedEttItems = new SparseArray<>(); - - public interface OutputListener { - void onPatParsed(List<PatItem> items); - void onPmtParsed(int programNumber, List<PmtItem> items); - void onMgtParsed(List<MgtItem> items); - void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber); - void onEitParsed(int sourceId, List<EitItem> items); - void onEttParsed(int sourceId, List<EttItem> descriptions); - void onSdtParsed(List<SdtItem> items); - } - - private final OutputListener mListener; - - public SectionParser(OutputListener listener) { - mListener = listener; - } - - public void parseSections(ByteArrayBuffer data) { - int pos = 0; - while (pos + 3 <= data.length()) { - if ((data.byteAt(pos) & 0xff) == 0xff) { - // Clear stuffing bytes according to H222.0 section 2.4.4. - data.setLength(0); - break; - } - int sectionLength = - (((data.byteAt(pos + 1) & 0x0f) << 8) | (data.byteAt(pos + 2) & 0xff)) + 3; - if (pos + sectionLength > data.length()) { - break; - } - if (DEBUG) { - Log.d(TAG, "parseSections 0x" + Integer.toHexString(data.byteAt(pos) & 0xff)); - } - parseSection(Arrays.copyOfRange(data.buffer(), pos, pos + sectionLength)); - pos += sectionLength; - } - if (mListener != null) { - for (int i = 0; i < mParsedEttItems.size(); ++i) { - int sourceId = mParsedEttItems.keyAt(i); - List<EttItem> descriptions = mParsedEttItems.valueAt(i); - mListener.onEttParsed(sourceId, descriptions); - } - } - mParsedEttItems.clear(); - } - - public void resetVersionNumbers() { - mSectionVersionMap.clear(); - } - - private void parseSection(byte[] data) { - if (!checkSanity(data)) { - Log.d(TAG, "Bad CRC!"); - return; - } - PsipSection section = PsipSection.create(data); - if (section == null) { - return; - } - - // The currentNextIndicator indicates that the section sent is currently applicable. - if (!section.getCurrentNextIndicator()) { - return; - } - int versionNumber = (data[5] & 0x3e) >> 1; - Integer oldVersionNumber = mSectionVersionMap.get(section); - - // The versionNumber shall be incremented when a change in the information carried within - // the section occurs. - if (oldVersionNumber != null && versionNumber == oldVersionNumber) { - return; - } - boolean result = false; - switch (data[0]) { - case TABLE_ID_PAT: - result = parsePAT(data); - break; - case TABLE_ID_PMT: - result = parsePMT(data); - break; - case TABLE_ID_MGT: - result = parseMGT(data); - break; - case TABLE_ID_TVCT: - case TABLE_ID_CVCT: - result = parseVCT(data); - break; - case TABLE_ID_EIT: - result = parseEIT(data); - break; - case TABLE_ID_ETT: - result = parseETT(data); - break; - case TABLE_ID_SDT: - result = parseSDT(data); - break; - case TABLE_ID_DVB_ACTUAL_P_F_EIT: - case TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT: - result = parseDVBEIT(data); - break; - default: - break; - } - if (result) { - mSectionVersionMap.put(section, versionNumber); - } - } - - private boolean parsePAT(byte[] data) { - if (DEBUG) { - Log.d(TAG, "PAT is discovered."); - } - int pos = 8; - - List<PatItem> results = new ArrayList<>(); - for (; pos < data.length - 4; pos = pos + 4) { - if (pos > data.length - 4 - 4) { - Log.e(TAG, "Broken PAT."); - return false; - } - int programNo = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int pmtPid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); - results.add(new PatItem(programNo, pmtPid)); - } - if (mListener != null) { - mListener.onPatParsed(results); - } - return true; - } - - private boolean parsePMT(byte[] data) { - int table_id_ext = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - if (DEBUG) { - Log.d(TAG, "PMT is discovered. programNo = " + table_id_ext); - } - if (data.length <= 11) { - Log.e(TAG, "Broken PMT."); - return false; - } - int pcrPid = (data[8] & 0x1f) << 8 | data[9]; - int programInfoLen = (data[10] & 0x0f) << 8 | data[11]; - int pos = 12; - List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + programInfoLen); - pos += programInfoLen; - if (DEBUG) { - Log.d(TAG, "PMT descriptors size: " + descriptors.size()); - } - List<PmtItem> results = new ArrayList<>(); - for (; pos < data.length - 4;) { - if (pos < 0) { - Log.e(TAG, "Broken PMT."); - return false; - } - int streamType = data[pos] & 0xff; - int esPid = (data[pos + 1] & 0x1f) << 8 | (data[pos + 2] & 0xff); - int esInfoLen = (data[pos + 3] & 0xf) << 8 | (data[pos + 4] & 0xff); - if (data.length < pos + esInfoLen + 5) { - Log.e(TAG, "Broken PMT."); - return false; - } - descriptors = parseDescriptors(data, pos + 5, pos + 5 + esInfoLen); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - PmtItem pmtItem = new PmtItem(streamType, esPid, audioTracks, captionTracks); - if (DEBUG) { - Log.d(TAG, "PMT " + pmtItem + " descriptors size: " + descriptors.size()); - } - results.add(pmtItem); - pos = pos + esInfoLen + 5; - } - results.add(new PmtItem(PmtItem.ES_PID_PCR, pcrPid, null, null)); - if (mListener != null) { - mListener.onPmtParsed(table_id_ext, results); - } - return true; - } - - private boolean parseMGT(byte[] data) { - // For details of the structure for MGT, see ATSC A/65 Table 6.2. - if (DEBUG) { - Log.d(TAG, "MGT is discovered."); - } - if (data.length <= 10) { - Log.e(TAG, "Broken MGT."); - return false; - } - int tablesDefined = ((data[9] & 0xff) << 8) | (data[10] & 0xff); - int pos = 11; - List<MgtItem> results = new ArrayList<>(); - for (int i = 0; i < tablesDefined; ++i) { - if (data.length <= pos + 10) { - Log.e(TAG, "Broken MGT."); - return false; - } - int tableType = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int tableTypePid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); - int descriptorsLength = ((data[pos + 9] & 0x0f) << 8) | (data[pos + 10] & 0xff); - pos += 11 + descriptorsLength; - results.add(new MgtItem(tableType, tableTypePid)); - } - // Skip the remaining descriptor part which we don't use. - - if (mListener != null) { - mListener.onMgtParsed(results); - } - return true; - } - - private boolean parseVCT(byte[] data) { - // For details of the structure for VCT, see ATSC A/65 Table 6.4 and 6.8. - if (DEBUG) { - Log.d(TAG, "VCT is discovered."); - } - if (data.length <= 9) { - Log.e(TAG, "Broken VCT."); - return false; - } - int numChannelsInSection = (data[9] & 0xff); - int sectionNumber = (data[6] & 0xff); - int lastSectionNumber = (data[7] & 0xff); - if (sectionNumber > lastSectionNumber) { - // According to section 6.3.1 of the spec ATSC A/65, - // last section number is the largest section number. - Log.w(TAG, "Invalid VCT. Section Number " + sectionNumber + " > Last Section Number " - + lastSectionNumber); - return false; - } - int pos = 10; - List<VctItem> results = new ArrayList<>(); - for (int i = 0; i < numChannelsInSection; ++i) { - if (data.length <= pos + 31) { - Log.e(TAG, "Broken VCT."); - return false; - } - String shortName = ""; - int shortNameSize = getShortNameSize(data, pos); - try { - shortName = new String( - Arrays.copyOfRange(data, pos, pos + shortNameSize), "UTF-16"); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Broken VCT.", e); - return false; - } - if ((data[pos + 14] & 0xf0) != 0xf0) { - Log.e(TAG, "Broken VCT."); - return false; - } - int majorNumber = ((data[pos + 14] & 0x0f) << 6) | ((data[pos + 15] & 0xff) >> 2); - int minorNumber = ((data[pos + 15] & 0x03) << 8) | (data[pos + 16] & 0xff); - if ((majorNumber & 0x3f0) == 0x3f0) { - // If the six MSBs are 111111, these indicate that there is only one-part channel - // number. To see details, refer A/65 Section 6.3.2. - majorNumber = ((majorNumber & 0xf) << 10) + minorNumber; - minorNumber = 0; - } - int channelTsid = ((data[pos + 22] & 0xff) << 8) | (data[pos + 23] & 0xff); - int programNumber = ((data[pos + 24] & 0xff) << 8) | (data[pos + 25] & 0xff); - boolean accessControlled = (data[pos + 26] & 0x20) != 0; - boolean hidden = (data[pos + 26] & 0x10) != 0; - int serviceType = (data[pos + 27] & 0x3f); - int sourceId = ((data[pos + 28] & 0xff) << 8) | (data[pos + 29] & 0xff); - int descriptorsPos = pos + 32; - int descriptorsLength = ((data[pos + 30] & 0x03) << 8) | (data[pos + 31] & 0xff); - pos += 32 + descriptorsLength; - if (data.length < pos) { - Log.e(TAG, "Broken VCT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - String longName = null; - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ExtendedChannelNameDescriptor) { - ExtendedChannelNameDescriptor extendedChannelNameDescriptor = - (ExtendedChannelNameDescriptor) descriptor; - longName = extendedChannelNameDescriptor.getLongChannelName(); - break; - } - } - if (DEBUG) { - Log.d(TAG, String.format( - "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d " - + "channel: %d-%d encrypted: %b hidden: %b, descriptors: %d", - shortName, longName, serviceType, channelTsid, programNumber, majorNumber, - minorNumber, accessControlled, hidden, descriptors.size())); - } - if (!accessControlled && !hidden && (serviceType == Channel.SERVICE_TYPE_ATSC_AUDIO || - serviceType == Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION || - serviceType == Channel.SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE)) { - // Hide hidden, encrypted, or unsupported ATSC service type channels - results.add(new VctItem(shortName, longName, serviceType, channelTsid, - programNumber, majorNumber, minorNumber, sourceId)); - } - } - // Skip the remaining descriptor part which we don't use. - - if (mListener != null) { - mListener.onVctParsed(results, sectionNumber, lastSectionNumber); - } - return true; - } - - private boolean parseEIT(byte[] data) { - // For details of the structure for EIT, see ATSC A/65 Table 6.11. - if (DEBUG) { - Log.d(TAG, "EIT is discovered."); - } - if (data.length <= 9) { - Log.e(TAG, "Broken EIT."); - return false; - } - int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int numEventsInSection = (data[9] & 0xff); - - int pos = 10; - List<EitItem> results = new ArrayList<>(); - for (int i = 0; i < numEventsInSection; ++i) { - if (data.length <= pos + 9) { - Log.e(TAG, "Broken EIT."); - return false; - } - if ((data[pos] & 0xc0) != 0xc0) { - Log.e(TAG, "Broken EIT."); - return false; - } - int eventId = ((data[pos] & 0x3f) << 8) + (data[pos + 1] & 0xff); - long startTime = ((data[pos + 2] & (long) 0xff) << 24) | ((data[pos + 3] & 0xff) << 16) - | ((data[pos + 4] & 0xff) << 8) | (data[pos + 5] & 0xff); - int lengthInSecond = ((data[pos + 6] & 0x0f) << 16) - | ((data[pos + 7] & 0xff) << 8) | (data[pos + 8] & 0xff); - int titleLength = (data[pos + 9] & 0xff); - if (data.length <= pos + 10 + titleLength + 1) { - Log.e(TAG, "Broken EIT."); - return false; - } - String titleText = ""; - if (titleLength > 0) { - titleText = extractText(data, pos + 10); - } - if ((data[pos + 10 + titleLength] & 0xf0) != 0xf0) { - Log.e(TAG, "Broken EIT."); - return false; - } - int descriptorsLength = ((data[pos + 10 + titleLength] & 0x0f) << 8) - | (data[pos + 10 + titleLength + 1] & 0xff); - int descriptorsPos = pos + 10 + titleLength + 2; - if (data.length < descriptorsPos + descriptorsLength) { - Log.e(TAG, "Broken EIT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - if (DEBUG) { - Log.d(TAG, String.format("EIT descriptors size: %d", descriptors.size())); - } - String contentRating = generateContentRating(descriptors); - String broadcastGenre = generateBroadcastGenre(descriptors); - String canonicalGenre = generateCanonicalGenre(descriptors); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - pos += 10 + titleLength + 2 + descriptorsLength; - results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, - startTime, lengthInSecond, contentRating, audioTracks, captionTracks, - broadcastGenre, canonicalGenre, null)); - } - if (mListener != null) { - mListener.onEitParsed(sourceId, results); - } - return true; - } - - private boolean parseETT(byte[] data) { - // For details of the structure for ETT, see ATSC A/65 Table 6.13. - if (DEBUG) { - Log.d(TAG, "ETT is discovered."); - } - if (data.length <= 12) { - Log.e(TAG, "Broken ETT."); - return false; - } - int sourceId = ((data[9] & 0xff) << 8) | (data[10] & 0xff); - int eventId = (((data[11] & 0xff) << 8) | (data[12] & 0xff)) >> 2; - String text = extractText(data, 13); - List<EttItem> ettItems = mParsedEttItems.get(sourceId); - if (ettItems == null) { - ettItems = new ArrayList<>(); - mParsedEttItems.put(sourceId, ettItems); - } - ettItems.add(new EttItem(eventId, text)); - return true; - } - - private boolean parseSDT(byte[] data) { - // For details of the structure for SDT, see DVB Document A038 Table 5. - if (DEBUG) { - Log.d(TAG, "SDT id discovered"); - } - if (data.length <= 11) { - Log.e(TAG, "Broken SDT."); - return false; - } - if ((data[1] & 0x80) >> 7 != 1) { - Log.e(TAG, "Broken SDT, section syntax indicator error."); - return false; - } - int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); - int transportStreamId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int originalNetworkId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); - int pos = 11; - if (sectionLength + 3 > data.length) { - Log.e(TAG, "Broken SDT."); - } - List<SdtItem> sdtItems = new ArrayList<>(); - while (pos + 9 < data.length) { - int serviceId = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int descriptorsLength = ((data[pos + 3] & 0x0f) << 8) | (data[pos + 4] & 0xff); - pos += 5; - List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + descriptorsLength); - List<ServiceDescriptor> serviceDescriptors = generateServiceDescriptors(descriptors); - String serviceName = ""; - String serviceProviderName = ""; - int serviceType = 0; - for (ServiceDescriptor serviceDescriptor : serviceDescriptors) { - serviceName = serviceDescriptor.getServiceName(); - serviceProviderName = serviceDescriptor.getServiceProviderName(); - serviceType = serviceDescriptor.getServiceType(); - } - if (serviceDescriptors.size() > 0) { - sdtItems.add(new SdtItem(serviceName, serviceProviderName, serviceType, serviceId, - originalNetworkId)); - } - pos += descriptorsLength; - } - if (mListener != null) { - mListener.onSdtParsed(sdtItems); - } - return true; - } - - private boolean parseDVBEIT(byte[] data) { - // For details of the structure for DVB ETT, see DVB Document A038 Table 7. - if (DEBUG) { - Log.d(TAG, "DVB EIT is discovered."); - } - if (data.length < 18) { - Log.e(TAG, "Broken DVB EIT."); - return false; - } - int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); - int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int transportStreamId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); - int originalNetworkId = ((data[10] & 0xff) << 8) | (data[11] & 0xff); - - int pos = 14; - List<EitItem> results = new ArrayList<>(); - while (pos + 12 < data.length) { - int eventId = ((data[pos] & 0xff) << 8) + (data[pos + 1] & 0xff); - float modifiedJulianDate = ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff); - int startYear = (int) ((modifiedJulianDate - 15078.2f) / 365.25f); - int mjdMonth = (int) ((modifiedJulianDate - 14956.1f - - (int) (startYear * 365.25f)) / 30.6001f); - int startDay = (int) modifiedJulianDate - 14956 - (int) (startYear * 365.25f) - - (int) (mjdMonth * 30.6001f); - int startMonth = mjdMonth - 1; - if (mjdMonth == 14 || mjdMonth == 15) { - startYear += 1; - startMonth -= 12; - } - int startHour = ((data[pos + 4] & 0xf0) >> 4) * 10 + (data[pos + 4] & 0x0f); - int startMinute = ((data[pos + 5] & 0xf0) >> 4) * 10 + (data[pos + 5] & 0x0f); - int startSecond = ((data[pos + 6] & 0xf0) >> 4) * 10 + (data[pos + 6] & 0x0f); - Calendar calendar = Calendar.getInstance(); - startYear += 1900; - calendar.set(startYear, startMonth, startDay, startHour, startMinute, startSecond); - long startTime = ConvertUtils.convertUnixEpochToGPSTime( - calendar.getTimeInMillis() / 1000); - int durationInSecond = (((data[pos + 7] & 0xf0) >> 4) * 10 - + (data[pos + 7] & 0x0f)) * 3600 - + (((data[pos + 8] & 0xf0) >> 4) * 10 + (data[pos + 8] & 0x0f)) * 60 - + (((data[pos + 9] & 0xf0) >> 4) * 10 + (data[pos + 9] & 0x0f)); - int descriptorsLength = ((data[pos + 10] & 0x0f) << 8) - | (data[pos + 10 + 1] & 0xff); - int descriptorsPos = pos + 10 + 2; - if (data.length < descriptorsPos + descriptorsLength) { - Log.e(TAG, "Broken EIT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - if (DEBUG) { - Log.d(TAG, String.format("DVB EIT descriptors size: %d", descriptors.size())); - } - // TODO: Add logic to generating content rating for dvb. See DVB document 6.2.28 for - // details. Content rating here will be null - String contentRating = generateContentRating(descriptors); - // TODO: Add logic for generating genre for dvb. See DVB document 6.2.9 for details. - // Genre here will be null here. - String broadcastGenre = generateBroadcastGenre(descriptors); - String canonicalGenre = generateCanonicalGenre(descriptors); - String titleText = generateShortEventName(descriptors); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - pos += 12 + descriptorsLength; - results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, - startTime, durationInSecond, contentRating, audioTracks, captionTracks, - broadcastGenre, canonicalGenre, null)); - } - if (mListener != null) { - mListener.onEitParsed(sourceId, results); - } - return true; - } - - private static List<AtscAudioTrack> generateAudioTracks(List<TsDescriptor> descriptors) { - // The list of audio tracks sent is located at both AC3 Audio descriptor and ISO 639 - // Language descriptor. - List<AtscAudioTrack> ac3Tracks = new ArrayList<>(); - List<AtscAudioTrack> iso639LanguageTracks = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof Ac3AudioDescriptor) { - Ac3AudioDescriptor audioDescriptor = - (Ac3AudioDescriptor) descriptor; - AtscAudioTrack audioTrack = new AtscAudioTrack(); - if (audioDescriptor.getLanguage() != null) { - audioTrack.language = audioDescriptor.getLanguage(); - } - if (audioTrack.language == null) { - audioTrack.language = ""; - } - audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED; - audioTrack.channelCount = audioDescriptor.getNumChannels(); - audioTrack.sampleRate = audioDescriptor.getSampleRate(); - ac3Tracks.add(audioTrack); - } - } - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof Iso639LanguageDescriptor) { - Iso639LanguageDescriptor iso639LanguageDescriptor = - (Iso639LanguageDescriptor) descriptor; - iso639LanguageTracks.addAll(iso639LanguageDescriptor.getAudioTracks()); - } - } - - // An AC3 audio stream descriptor only has a audio channel count and a audio sample rate - // while a ISO 639 Language descriptor only has a audio type, which describes a main use - // case of its audio track. - // Some channels contain only AC3 audio stream descriptors with valid language values. - // Other channels contain both an AC3 audio stream descriptor and a ISO 639 Language - // descriptor per audio track, and those AC3 audio stream descriptors often have a null - // value of language field. - // Combines two descriptors into one in order to gather more audio track specific - // information as much as possible. - List<AtscAudioTrack> tracks = new ArrayList<>(); - if (!ac3Tracks.isEmpty() && !iso639LanguageTracks.isEmpty() - && ac3Tracks.size() != iso639LanguageTracks.size()) { - // This shouldn't be happen. In here, it handles two cases. The first case is that the - // only one type of descriptors arrives. The second case is that the two types of - // descriptors have the same number of tracks. - Log.e(TAG, "AC3 audio stream descriptors size != ISO 639 Language descriptors size"); - return tracks; - } - int size = Math.max(ac3Tracks.size(), iso639LanguageTracks.size()); - for (int i = 0; i < size; ++i) { - AtscAudioTrack audioTrack = null; - if (i < ac3Tracks.size()) { - audioTrack = ac3Tracks.get(i); - } - if (i < iso639LanguageTracks.size()) { - if (audioTrack == null) { - audioTrack = iso639LanguageTracks.get(i); - } else { - AtscAudioTrack iso639LanguageTrack = iso639LanguageTracks.get(i); - if (audioTrack.language == null || TextUtils.equals(audioTrack.language, "")) { - audioTrack.language = iso639LanguageTrack.language; - } - audioTrack.audioType = iso639LanguageTrack.audioType; - } - } - String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.language); - if (language != null) { - audioTrack.language = language; - } - tracks.add(audioTrack); - } - return tracks; - } - - private static List<AtscCaptionTrack> generateCaptionTracks(List<TsDescriptor> descriptors) { - List<AtscCaptionTrack> services = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof CaptionServiceDescriptor) { - CaptionServiceDescriptor captionServiceDescriptor = - (CaptionServiceDescriptor) descriptor; - services.addAll(captionServiceDescriptor.getCaptionTracks()); - } - } - return services; - } - - @VisibleForTesting - static String generateContentRating(List<TsDescriptor> descriptors) { - Set<String> contentRatings = new ArraySet<>(); - List<RatingRegion> usRatingRegions = getRatingRegions(descriptors, RATING_REGION_US_TV); - List<RatingRegion> krRatingRegions = getRatingRegions(descriptors, RATING_REGION_KR_TV); - for (RatingRegion region : usRatingRegions) { - String contentRating = getUsRating(region); - if (contentRating != null) { - contentRatings.add(contentRating); - } - } - for (RatingRegion region : krRatingRegions) { - String contentRating = getKrRating(region); - if (contentRating != null) { - contentRatings.add(contentRating); - } - } - return TextUtils.join(",", contentRatings); - } - - /** - * Gets a list of {@link RatingRegion} in the specific region. - * - * @param descriptors {@link TsDescriptor} list which may contains rating information - * @param region the specific region - * @return a list of {@link RatingRegion} in the specific region - */ - private static List<RatingRegion> getRatingRegions(List<TsDescriptor> descriptors, int region) { - List<RatingRegion> ratingRegions = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (!(descriptor instanceof ContentAdvisoryDescriptor)) { - continue; - } - ContentAdvisoryDescriptor contentAdvisoryDescriptor = - (ContentAdvisoryDescriptor) descriptor; - for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) { - if (ratingRegion.getName() == region) { - ratingRegions.add(ratingRegion); - } - } - } - return ratingRegions; - } - - /** - * Gets US content rating and subratings (if any). - * - * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. - * @return A string representing the US content rating and subratings. The format of the string - * is defined in {@link TvContentRating}. null, if no such a string exists. - */ - private static String getUsRating(RatingRegion ratingRegion) { - if (ratingRegion.getName() != RATING_REGION_US_TV) { - return null; - } - List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); - String rating = null; - int ratingIndex = VALUE_US_TV_NONE; - List<String> subratings = new ArrayList<>(); - for (RegionalRating index : regionalRatings) { - // See Table 3 of ANSI-CEA-766-D - int dimension = index.getDimension(); - int value = index.getRating(); - switch (dimension) { - // According to Table 6.27 of ATSC A65, - // the dimensions shall be in increasing order. - // Therefore, rating and ratingIndex are assigned before any corresponding - // subrating. - case DIMENSION_US_TV_RATING: - if (value >= VALUE_US_TV_G && value < RATING_REGION_TABLE_US_TV.length) { - rating = RATING_REGION_TABLE_US_TV[value]; - ratingIndex = value; - } - break; - case DIMENSION_US_TV_D: - if (value == 1 - && (ratingIndex == VALUE_US_TV_PG || ratingIndex == VALUE_US_TV_14)) { - // US_TV_D is applicable to US_TV_PG and US_TV_14 - subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); - } - break; - case DIMENSION_US_TV_L: - case DIMENSION_US_TV_S: - case DIMENSION_US_TV_V: - if (value == 1 - && ratingIndex >= VALUE_US_TV_PG - && ratingIndex <= VALUE_US_TV_MA) { - // US_TV_L, US_TV_S, and US_TV_V are applicable to - // US_TV_PG, US_TV_14 and US_TV_MA - subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); - } - break; - case DIMENSION_US_TV_Y: - if (rating == null) { - if (value == VALUE_US_TV_Y) { - rating = STRING_US_TV_Y; - } else if (value == VALUE_US_TV_Y7) { - rating = STRING_US_TV_Y7; - } - } - break; - case DIMENSION_US_TV_FV: - if (STRING_US_TV_Y7.equals(rating) && value == 1) { - // US_TV_FV is applicable to US_TV_Y7 - subratings.add(STRING_US_TV_FV); - } - break; - case DIMENSION_US_MV_RATING: - if (value >= VALUE_US_MV_G && value <= VALUE_US_MV_X) { - if (value == VALUE_US_MV_X) { - // US_MV_X was replaced by US_MV_NC17 in 1990, - // and it's not supported by TvContentRating - value = VALUE_US_MV_NC17; - } - if (rating != null) { - // According to Table 3 of ANSI-CEA-766-D, - // DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING shall not be - // present in the same descriptor. - Log.w( - TAG, - "DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING are " - + "present in the same descriptor"); - } else { - return TvContentRating.createRating( - RATING_DOMAIN, - RATING_REGION_RATING_SYSTEM_US_MV, - RATING_REGION_TABLE_US_MV[value - 2]) - .flattenToString(); - } - } - break; - - default: - break; - } - } - if (rating == null) { - return null; - } - - String[] subratingArray = subratings.toArray(new String[subratings.size()]); - return TvContentRating.createRating( - RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_US_TV, rating, subratingArray) - .flattenToString(); - } - - /** - * Gets KR(South Korea) content rating. - * - * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. - * @return A string representing the KR content rating. The format of the string is defined in - * {@link TvContentRating}. null, if no such a string exists. - */ - private static String getKrRating(RatingRegion ratingRegion) { - if (ratingRegion.getName() != RATING_REGION_KR_TV) { - return null; - } - List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); - String rating = null; - for (RegionalRating index : regionalRatings) { - if (index.getDimension() == 0 - && index.getRating() >= 0 - && index.getRating() < RATING_REGION_TABLE_KR_TV.length) { - rating = RATING_REGION_TABLE_KR_TV[index.getRating()]; - break; - } - } - if (rating == null) { - return null; - } - return TvContentRating.createRating( - RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_KR_TV, rating) - .flattenToString(); - } - - private static String generateBroadcastGenre(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof GenreDescriptor) { - GenreDescriptor genreDescriptor = - (GenreDescriptor) descriptor; - return TextUtils.join(",", genreDescriptor.getBroadcastGenres()); - } - } - return null; - } - - private static String generateCanonicalGenre(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof GenreDescriptor) { - GenreDescriptor genreDescriptor = - (GenreDescriptor) descriptor; - return Genres.encode(genreDescriptor.getCanonicalGenres()); - } - } - return null; - } - - private static List<ServiceDescriptor> generateServiceDescriptors( - List<TsDescriptor> descriptors) { - List<ServiceDescriptor> serviceDescriptors = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ServiceDescriptor) { - ServiceDescriptor serviceDescriptor = (ServiceDescriptor) descriptor; - serviceDescriptors.add(serviceDescriptor); - } - } - return serviceDescriptors; - } - - private static String generateShortEventName(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ShortEventDescriptor) { - ShortEventDescriptor shortEventDescriptor = (ShortEventDescriptor) descriptor; - return shortEventDescriptor.getEventName(); - } - } - return ""; - } - - private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) { - // For details of the structure for descriptors, see ATSC A/65 Section 6.9. - List<TsDescriptor> descriptors = new ArrayList<>(); - if (data.length < limit) { - return descriptors; - } - int pos = offset; - while (pos + 1 < limit) { - int tag = data[pos] & 0xff; - int length = data[pos + 1] & 0xff; - if (length <= 0) { - break; - } - if (limit < pos + length + 2) { - break; - } - if (DEBUG) { - Log.d(TAG, String.format("Descriptor tag: %02x", tag)); - } - TsDescriptor descriptor = null; - switch (tag) { - case DESCRIPTOR_TAG_CONTENT_ADVISORY: - descriptor = parseContentAdvisory(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_CAPTION_SERVICE: - descriptor = parseCaptionService(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME: - descriptor = parseLongChannelName(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_GENRE: - descriptor = parseGenre(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_AC3_AUDIO_STREAM: - descriptor = parseAc3AudioStream(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_ISO639LANGUAGE: - descriptor = parseIso639Language(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_SERVICE: - descriptor = parseDvbService(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_SHORT_EVENT: - descriptor = parseDvbShortEvent(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_CONTENT: - descriptor = parseDvbContent(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_PARENTAL_RATING: - descriptor = parseDvbParentalRating(data, pos, pos + length + 2); - break; - - default: - } - if (descriptor != null) { - if (DEBUG) { - Log.d(TAG, "Descriptor parsed: " + descriptor); - } - descriptors.add(descriptor); - } - pos += length + 2; - } - return descriptors; - } - - private static Iso639LanguageDescriptor parseIso639Language(byte[] data, int pos, int limit) { - // For the details of the structure of ISO 639 language descriptor, - // see ISO13818-1 second edition Section 2.6.18. - pos += 2; - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - while (pos + 4 <= limit) { - if (limit <= pos + 3) { - Log.e(TAG, "Broken Iso639Language."); - return null; - } - String language = new String(data, pos, 3); - int audioType = data[pos + 3] & 0xff; - AtscAudioTrack audioTrack = new AtscAudioTrack(); - audioTrack.language = language; - audioTrack.audioType = audioType; - audioTracks.add(audioTrack); - pos += 4; - } - return new Iso639LanguageDescriptor(audioTracks); - } - - private static CaptionServiceDescriptor parseCaptionService(byte[] data, int pos, int limit) { - // For the details of the structure of caption service descriptor, - // see ATSC A/65 Section 6.9.2. - if (limit <= pos + 2) { - Log.e(TAG, "Broken CaptionServiceDescriptor."); - return null; - } - List<AtscCaptionTrack> services = new ArrayList<>(); - pos += 2; - int numberServices = data[pos] & 0x1f; - ++pos; - if (limit < pos + numberServices * 6) { - Log.e(TAG, "Broken CaptionServiceDescriptor."); - return null; - } - for (int i = 0; i < numberServices; ++i) { - String language = new String(Arrays.copyOfRange(data, pos, pos + 3)); - pos += 3; - boolean ccType = (data[pos] & 0x80) != 0; - if (!ccType) { - pos +=3; - continue; - } - int captionServiceNumber = data[pos] & 0x3f; - ++pos; - boolean easyReader = (data[pos] & 0x80) != 0; - boolean wideAspectRatio = (data[pos] & 0x40) != 0; - byte[] reserved = new byte[2]; - reserved[0] = (byte) (data[pos] << 2); - reserved[0] |= (byte) ((data[pos + 1] & 0xc0) >>> 6); - reserved[1] = (byte) ((data[pos + 1] & 0x3f) << 2); - pos += 2; - AtscCaptionTrack captionTrack = new AtscCaptionTrack(); - captionTrack.language = language; - captionTrack.serviceNumber = captionServiceNumber; - captionTrack.easyReader = easyReader; - captionTrack.wideAspectRatio = wideAspectRatio; - services.add(captionTrack); - } - return new CaptionServiceDescriptor(services); - } - - private static ContentAdvisoryDescriptor parseContentAdvisory(byte[] data, int pos, int limit) { - // For details of the structure for content advisory descriptor, see A/65 Table 6.27. - if (limit <= pos + 2) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int count = data[pos + 2] & 0x3f; - pos += 3; - List<RatingRegion> ratingRegions = new ArrayList<>(); - for (int i = 0; i < count; ++i) { - if (limit <= pos + 1) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - List<RegionalRating> indices = new ArrayList<>(); - int ratingRegion = data[pos] & 0xff; - int dimensionCount = data[pos + 1] & 0xff; - pos += 2; - int previousDimension = -1; - for (int j = 0; j < dimensionCount; ++j) { - if (limit <= pos + 1) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int dimensionIndex = data[pos] & 0xff; - int ratingValue = data[pos + 1] & 0x0f; - if (dimensionIndex <= previousDimension) { - // According to Table 6.27 of ATSC A65, - // the indices shall be in increasing order. - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - previousDimension = dimensionIndex; - pos += 2; - indices.add(new RegionalRating(dimensionIndex, ratingValue)); - } - if (limit <= pos) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int ratingDescriptionLength = data[pos] & 0xff; - ++pos; - if (limit < pos + ratingDescriptionLength) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - String ratingDescription = extractText(data, pos); - pos += ratingDescriptionLength; - ratingRegions.add(new RatingRegion(ratingRegion, ratingDescription, indices)); - } - return new ContentAdvisoryDescriptor(ratingRegions); - } - - private static ExtendedChannelNameDescriptor parseLongChannelName(byte[] data, int pos, - int limit) { - if (limit <= pos + 2) { - Log.e(TAG, "Broken ExtendedChannelName."); - return null; - } - pos += 2; - String text = extractText(data, pos); - if (text == null) { - Log.e(TAG, "Broken ExtendedChannelName."); - return null; - } - return new ExtendedChannelNameDescriptor(text); - } - - private static GenreDescriptor parseGenre(byte[] data, int pos, int limit) { - pos += 2; - int attributeCount = data[pos] & 0x1f; - if (limit <= pos + attributeCount) { - Log.e(TAG, "Broken Genre."); - return null; - } - HashSet<String> broadcastGenreSet = new HashSet<>(); - HashSet<String> canonicalGenreSet = new HashSet<>(); - for (int i = 0; i < attributeCount; ++i) { - ++pos; - int genreCode = data[pos] & 0xff; - if (genreCode < BROADCAST_GENRES_TABLE.length) { - String broadcastGenre = BROADCAST_GENRES_TABLE[genreCode]; - if (broadcastGenre != null && !broadcastGenreSet.contains(broadcastGenre)) { - broadcastGenreSet.add(broadcastGenre); - } - } - if (genreCode < CANONICAL_GENRES_TABLE.length) { - String canonicalGenre = CANONICAL_GENRES_TABLE[genreCode]; - if (canonicalGenre != null && !canonicalGenreSet.contains(canonicalGenre)) { - canonicalGenreSet.add(canonicalGenre); - } - } - } - return new GenreDescriptor(broadcastGenreSet.toArray(new String[broadcastGenreSet.size()]), - canonicalGenreSet.toArray(new String[canonicalGenreSet.size()])); - } - - private static TsDescriptor parseAc3AudioStream(byte[] data, int pos, int limit) { - // For details of the AC3 audio stream descriptor, see A/52 Table A4.1. - if (limit <= pos + 5) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - pos += 2; - byte sampleRateCode = (byte) ((data[pos] & 0xe0) >> 5); - byte bsid = (byte) (data[pos] & 0x1f); - ++pos; - byte bitRateCode = (byte) ((data[pos] & 0xfc) >> 2); - byte surroundMode = (byte) (data[pos] & 0x03); - ++pos; - byte bsmod = (byte) ((data[pos] & 0xe0) >> 5); - int numChannels = (data[pos] & 0x1e) >> 1; - boolean fullSvc = (data[pos] & 0x01) != 0; - ++pos; - byte langCod = data[pos]; - byte langCod2 = 0; - if (numChannels == 0) { - if (limit <= pos) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - ++pos; - langCod2 = data[pos]; - } - if (limit <= pos + 1) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - byte mainId = 0; - byte priority = 0; - byte asvcflags = 0; - ++pos; - if (bsmod < 2) { - mainId = (byte) ((data[pos] & 0xe0) >> 5); - priority = (byte) ((data[pos] & 0x18) >> 3); - if ((data[pos] & 0x07) != 0x07) { - Log.e(TAG, "Broken AC3 audio stream descriptor reserved failed"); - return null; - } - } else { - asvcflags = data[pos]; - } - - // See A/52B Table A3.6 num_channels. - int numEncodedChannels; - switch (numChannels) { - case 1: - case 8: - numEncodedChannels = 1; - break; - case 2: - case 9: - numEncodedChannels = 2; - break; - case 3: - case 4: - case 10: - numEncodedChannels = 3; - break; - case 5: - case 6: - case 11: - numEncodedChannels = 4; - break; - case 7: - case 12: - numEncodedChannels = 5; - break; - case 13: - numEncodedChannels = 6; - break; - default: - numEncodedChannels = 0; - break; - } - - if (limit <= pos + 1) { - Log.w(TAG, "Missing text and language fields on AC3 audio stream descriptor."); - return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, - numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, - null, null, null); - } - ++pos; - int textLen = (data[pos] & 0xfe) >> 1; - boolean textCode = (data[pos] & 0x01) != 0; - ++pos; - String text = ""; - if (textLen > 0) { - if (limit < pos + textLen) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - if (textCode) { - text = new String(data, pos, textLen); - } else { - text = new String(data, pos, textLen, Charset.forName("UTF-16")); - } - pos += textLen; - } - String language = null; - String language2 = null; - if (pos < limit) { - // Many AC3 audio stream descriptors skip the language fields. - boolean languageFlag1 = (data[pos] & 0x80) != 0; - boolean languageFlag2 = (data[pos] & 0x40) != 0; - if ((data[pos] & 0x3f) != 0x3f) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - if (pos + (languageFlag1 ? 3 : 0) + (languageFlag2 ? 3 : 0) > limit) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - ++pos; - if (languageFlag1) { - language = new String(data, pos, 3); - pos += 3; - } - if (languageFlag2) { - language2 = new String(data, pos, 3); - } - } - - return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, - numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, text, - language, language2); - } - - private static TsDescriptor parseDvbService(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 86. - if (limit < pos + 5) { - Log.e(TAG, "Broken service descriptor."); - return null; - } - pos += 2; - int serviceType = data[pos] & 0xff; - pos++; - int serviceProviderNameLength = data[pos] & 0xff; - pos++; - String serviceProviderName = extractTextFromDvb(data, pos, serviceProviderNameLength); - pos += serviceProviderNameLength; - int serviceNameLength = data[pos] & 0xff; - pos++; - String serviceName = extractTextFromDvb(data, pos, serviceNameLength); - return new ServiceDescriptor(serviceType, serviceProviderName, serviceName); - } - - private static TsDescriptor parseDvbShortEvent(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 91. - if (limit < pos + 7) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - pos += 2; - String language = new String(data, pos, 3); - int eventNameLength = data[pos + 3] & 0xff; - pos += 4; - if (pos + eventNameLength > limit) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - String eventName = new String(data, pos, eventNameLength); - pos += eventNameLength; - int textLength = data[pos] & 0xff; - if (pos + textLength > limit) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - pos++; - String text = new String(data, pos, textLength); - return new ShortEventDescriptor(language, eventName, text); - } - - private static TsDescriptor parseDvbContent(byte[] data, int pos, int limit) { - // TODO: According to DVB Document A038 Table 27 to add a parser for content descriptor to - // get content genre. - return null; - } - - private static TsDescriptor parseDvbParentalRating(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 81. - HashMap<String, Integer> ratings = new HashMap<>(); - pos += 2; - while (pos + 4 <= limit) { - String countryCode = new String(data, pos, 3); - int rating = data[pos + 3] & 0xff; - pos += 4; - if (rating > 15) { - // Rating > 15 means that the ratings is defined by broadcaster. - continue; - } - ratings.put(countryCode, rating + 3); - } - return new ParentalRatingDescriptor(ratings); - } - - private static int getShortNameSize(byte[] data, int offset) { - for (int i = 0; i < MAX_SHORT_NAME_BYTES; i += 2) { - if (data[offset + i] == 0 && data[offset + i + 1] == 0) { - return i; - } - } - return MAX_SHORT_NAME_BYTES; - } - - private static String extractText(byte[] data, int pos) { - if (data.length < pos) { - return null; - } - int numStrings = data[pos] & 0xff; - pos++; - for (int i = 0; i < numStrings; ++i) { - if (data.length <= pos + 3) { - Log.e(TAG, "Broken text."); - return null; - } - int numSegments = data[pos + 3] & 0xff; - pos += 4; - for (int j = 0; j < numSegments; ++j) { - if (data.length <= pos + 2) { - Log.e(TAG, "Broken text."); - return null; - } - int compressionType = data[pos] & 0xff; - int mode = data[pos + 1] & 0xff; - int numBytes = data[pos + 2] & 0xff; - if (data.length < pos + 3 + numBytes) { - Log.e(TAG, "Broken text."); - return null; - } - byte[] bytes = Arrays.copyOfRange(data, pos + 3, pos + 3 + numBytes); - if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) { - try { - switch (mode) { - case MODE_SELECTED_UNICODE_RANGE_1: - return new String(bytes, "ISO-8859-1"); - case MODE_SCSU: - return UnicodeDecompressor.decompress(bytes); - case MODE_UTF16: - return new String(bytes, "UTF-16"); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unsupported text format.", e); - } - } - pos += 3 + numBytes; - } - } - return null; - } - - private static String extractTextFromDvb(byte[] data, int pos, int length) { - // For details of DVB character set selection, see DVB Document A038 Annex A. - if (data.length < pos + length) { - return null; - } - try { - String charsetPrefix = "ISO-8859-"; - switch (data[0]) { - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x09: - case 0x0A: - case 0x0B: - String charset = charsetPrefix + String.valueOf(data[0] & 0xff + 4); - return new String(data, pos, length, charset); - case 0x10: - if (length < 3) { - Log.e(TAG, "Broken DVB text"); - return null; - } - int codeTable = data[pos + 2] & 0xff; - if (data[pos + 1] == 0 && codeTable > 0 && codeTable < 15) { - return new String( - data, pos, length, charsetPrefix + String.valueOf(codeTable)); - } else { - return new String(data, pos, length, "ISO-8859-1"); - } - case 0x11: - case 0x14: - case 0x15: - return new String(data, pos, length, "UTF-16BE"); - case 0x12: - return new String(data, pos, length, "EUC-KR"); - case 0x13: - return new String(data, pos, length, "GB2312"); - default: - return new String(data, pos, length, "ISO-8859-1"); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unsupported text format.", e); - } - return new String(data, pos, length); - } - - private static boolean checkSanity(byte[] data) { - if (data.length <= 1) { - return false; - } - boolean hasCRC = (data[1] & 0x80) != 0; // section_syntax_indicator - if (hasCRC) { - int crc = 0xffffffff; - for(byte b : data) { - int index = ((crc >> 24) ^ (b & 0xff)) & 0xff; - crc = CRC_TABLE[index] ^ (crc << 8); - } - if(crc != 0){ - return false; - } - } - return true; - } -} diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java deleted file mode 100644 index 7cdb534e..00000000 --- a/src/com/android/tv/tuner/ts/TsParser.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.ts; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.tv.tuner.data.PsiData.PatItem; -import com.android.tv.tuner.data.PsiData.PmtItem; -import com.android.tv.tuner.data.PsipData.EitItem; -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.TunerChannel; -import com.android.tv.tuner.ts.SectionParser.OutputListener; -import com.android.tv.tuner.util.ByteArrayBuffer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeSet; - -/** - * Parses MPEG-2 TS packets. - */ -public class TsParser { - private static final String TAG = "TsParser"; - private static final boolean DEBUG = false; - - public static final int ATSC_SI_BASE_PID = 0x1ffb; - public static final int PAT_PID = 0x0000; - public static final int DVB_SDT_PID = 0x0011; - public static final int DVB_EIT_PID = 0x0012; - private static final int TS_PACKET_START_CODE = 0x47; - private static final int TS_PACKET_TEI_MASK = 0x80; - private static final int TS_PACKET_SIZE = 188; - - /* - * Using a SparseArray removes the need to auto box the int key for mStreamMap - * in feedTdPacket which is called 100 times a second. This greatly reduces the - * number of objects created and the frequency of garbage collection. - * Other maps might be suitable for a SparseArray, but the performance - * trade offs must be considered carefully. - * mStreamMap is the only one called at such a high rate. - */ - private final SparseArray<Stream> mStreamMap = new SparseArray<>(); - private final Map<Integer, VctItem> mSourceIdToVctItemMap = new HashMap<>(); - private final Map<Integer, String> mSourceIdToVctItemDescriptionMap = new HashMap<>(); - private final Map<Integer, VctItem> mProgramNumberToVctItemMap = new HashMap<>(); - private final Map<Integer, List<PmtItem>> mProgramNumberToPMTMap = new HashMap<>(); - private final Map<Integer, List<EitItem>> mSourceIdToEitMap = new HashMap<>(); - private final Map<Integer, SdtItem> mProgramNumberToSdtItemMap = new HashMap<>(); - private final Map<EventSourceEntry, List<EitItem>> mEitMap = new HashMap<>(); - private final Map<EventSourceEntry, List<EttItem>> mETTMap = new HashMap<>(); - private final TreeSet<Integer> mEITPids = new TreeSet<>(); - private final TreeSet<Integer> mETTPids = new TreeSet<>(); - private final SparseBooleanArray mProgramNumberHandledStatus = new SparseBooleanArray(); - private final SparseBooleanArray mVctItemHandledStatus = new SparseBooleanArray(); - private final TsOutputListener mListener; - private final boolean mIsDvbSignal; - - private int mVctItemCount; - private int mHandledVctItemCount; - private int mVctSectionParsedCount; - private boolean[] mVctSectionParsed; - - public interface TsOutputListener { - void onPatDetected(List<PatItem> items); - void onEitPidDetected(int pid); - void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems); - void onEitItemParsed(VctItem channel, List<EitItem> items); - void onEttPidDetected(int pid); - void onAllVctItemsParsed(); - void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems); - } - - private abstract class Stream { - private static final int INVALID_CONTINUITY_COUNTER = -1; - private static final int NUM_CONTINUITY_COUNTER = 16; - - protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER; - protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE); - - public void feedData(byte[] data, int continuityCounter, boolean startIndicator) { - if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) { - mPacket.setLength(0); - } - mContinuityCounter = continuityCounter; - handleData(data, startIndicator); - } - - protected abstract void handleData(byte[] data, boolean startIndicator); - protected abstract void resetDataVersions(); - } - - private class SectionStream extends Stream { - private final SectionParser mSectionParser; - private final int mPid; - - public SectionStream(int pid) { - mPid = pid; - mSectionParser = new SectionParser(mSectionListener); - } - - @Override - protected void handleData(byte[] data, boolean startIndicator) { - int startPos = 0; - if (mPacket.length() == 0) { - if (startIndicator) { - startPos = (data[0] & 0xff) + 1; - } else { - // Don't know where the section starts yet. Wait until start indicator is on. - return; - } - } else { - if (startIndicator) { - startPos = 1; - } - } - - // When a broken packet is encountered, parsing will stop and return right away. - if (startPos >= data.length) { - mPacket.setLength(0); - return; - } - mPacket.append(data, startPos, data.length - startPos); - mSectionParser.parseSections(mPacket); - } - - @Override - protected void resetDataVersions() { - mSectionParser.resetVersionNumbers(); - } - - private final OutputListener mSectionListener = new OutputListener() { - @Override - public void onPatParsed(List<PatItem> items) { - for (PatItem i : items) { - startListening(i.getPmtPid()); - } - if (mListener != null) { - mListener.onPatDetected(items); - } - } - - @Override - public void onPmtParsed(int programNumber, List<PmtItem> items) { - mProgramNumberToPMTMap.put(programNumber, items); - if (DEBUG) { - Log.d(TAG, "onPMTParsed, programNo " + programNumber + " handledStatus is " - + mProgramNumberHandledStatus.get(programNumber, false)); - } - int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber); - if (statusIndex < 0) { - mProgramNumberHandledStatus.put(programNumber, false); - } - if (!mProgramNumberHandledStatus.get(programNumber)) { - VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber); - if (vctItem != null) { - // When PMT is parsed later than VCT. - mProgramNumberHandledStatus.put(programNumber, true); - handleVctItem(vctItem, items); - mHandledVctItemCount++; - if (mHandledVctItemCount >= mVctItemCount - && mVctSectionParsedCount >= mVctSectionParsed.length - && mListener != null) { - mListener.onAllVctItemsParsed(); - } - } - SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber); - if (sdtItem != null) { - // When PMT is parsed later than SDT. - mProgramNumberHandledStatus.put(programNumber, true); - handleSdtItem(sdtItem, items); - } - } - } - - @Override - public void onMgtParsed(List<MgtItem> items) { - for (MgtItem i : items) { - if (mStreamMap.get(i.getTableTypePid()) != null) { - continue; - } - if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START - && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) { - startListening(i.getTableTypePid()); - mEITPids.add(i.getTableTypePid()); - if (mListener != null) { - mListener.onEitPidDetected(i.getTableTypePid()); - } - } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT || - (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START - && i.getTableType() <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) { - startListening(i.getTableTypePid()); - mETTPids.add(i.getTableTypePid()); - if (mListener != null) { - mListener.onEttPidDetected(i.getTableTypePid()); - } - } - } - } - - @Override - public void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber) { - if (mVctSectionParsed == null) { - mVctSectionParsed = new boolean[lastSectionNumber + 1]; - } else if (mVctSectionParsed[sectionNumber]) { - // The current section was handled before. - if (DEBUG) { - Log.d(TAG, "Duplicate VCT section found."); - } - return; - } - mVctSectionParsed[sectionNumber] = true; - mVctSectionParsedCount++; - mVctItemCount += items.size(); - for (VctItem i : items) { - if (DEBUG) Log.d(TAG, "onVCTParsed " + i); - if (i.getSourceId() != 0) { - mSourceIdToVctItemMap.put(i.getSourceId(), i); - i.setDescription(mSourceIdToVctItemDescriptionMap.get(i.getSourceId())); - } - int programNumber = i.getProgramNumber(); - mProgramNumberToVctItemMap.put(programNumber, i); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - mProgramNumberHandledStatus.put(programNumber, true); - handleVctItem(i, pmtList); - mHandledVctItemCount++; - if (mHandledVctItemCount >= mVctItemCount - && mVctSectionParsedCount >= mVctSectionParsed.length - && mListener != null) { - mListener.onAllVctItemsParsed(); - } - } else { - mProgramNumberHandledStatus.put(programNumber, false); - Log.i(TAG, "onVCTParsed, but PMT for programNo " + programNumber - + " is not found yet."); - } - } - } - - @Override - public void onEitParsed(int sourceId, List<EitItem> items) { - if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId); - EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); - mEitMap.put(entry, items); - handleEvents(sourceId); - } - - @Override - public void onEttParsed(int sourceId, List<EttItem> descriptions) { - if (DEBUG) { - Log.d(TAG, String.format("onETTParsed sourceId: %d, descriptions.size(): %d", - sourceId, descriptions.size())); - } - for (EttItem item : descriptions) { - if (item.eventId == 0) { - // Channel description - mSourceIdToVctItemDescriptionMap.put(sourceId, item.text); - VctItem vctItem = mSourceIdToVctItemMap.get(sourceId); - if (vctItem != null) { - vctItem.setDescription(item.text); - List<PmtItem> pmtItems = - mProgramNumberToPMTMap.get(vctItem.getProgramNumber()); - if (pmtItems != null) { - handleVctItem(vctItem, pmtItems); - } - } - } - } - - // Event Information description - EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); - mETTMap.put(entry, descriptions); - handleEvents(sourceId); - } - - @Override - public void onSdtParsed(List<SdtItem> sdtItems) { - for (SdtItem sdtItem : sdtItems) { - if (DEBUG) Log.d(TAG, "onSdtParsed " + sdtItem); - int programNumber = sdtItem.getServiceId(); - mProgramNumberToSdtItemMap.put(programNumber, sdtItem); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - mProgramNumberHandledStatus.put(programNumber, true); - handleSdtItem(sdtItem, pmtList); - } else { - mProgramNumberHandledStatus.put(programNumber, false); - Log.i(TAG, "onSdtParsed, but PMT for programNo " + programNumber - + " is not found yet."); - } - } - } - }; - } - - private static class EventSourceEntry { - public final int pid; - public final int sourceId; - - public EventSourceEntry(int pid, int sourceId) { - this.pid = pid; - this.sourceId = sourceId; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + pid; - result = 31 * result + sourceId; - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof EventSourceEntry) { - EventSourceEntry another = (EventSourceEntry) obj; - return pid == another.pid && sourceId == another.sourceId; - } - return false; - } - } - - private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "handleVctItem " + channel); - } - if (mListener != null) { - mListener.onVctItemParsed(channel, pmtItems); - } - int sourceId = channel.getSourceId(); - int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId); - if (statusIndex < 0) { - mVctItemHandledStatus.put(sourceId, false); - return; - } - if (!mVctItemHandledStatus.valueAt(statusIndex)) { - List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId); - if (eitItems != null) { - // When VCT is parsed later than EIT. - mVctItemHandledStatus.put(sourceId, true); - handleEitItems(channel, eitItems); - } - } - } - - private void handleEitItems(VctItem channel, List<EitItem> items) { - if (mListener != null) { - mListener.onEitItemParsed(channel, items); - } - } - - private void handleSdtItem(SdtItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "handleSdtItem " + channel); - } - if (mListener != null) { - mListener.onSdtItemParsed(channel, pmtItems); - } - } - - private void handleEvents(int sourceId) { - Map<Integer, EitItem> itemSet = new HashMap<>(); - for (int pid : mEITPids) { - List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId)); - if (eitItems != null) { - for (EitItem item : eitItems) { - item.setDescription(null); - itemSet.put(item.getEventId(), item); - } - } - } - for (int pid : mETTPids) { - List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId)); - if (ettItems != null) { - for (EttItem ettItem : ettItems) { - if (ettItem.eventId != 0) { - EitItem item = itemSet.get(ettItem.eventId); - if (item != null) { - item.setDescription(ettItem.text); - } - } - } - } - } - List<EitItem> items = new ArrayList<>(itemSet.values()); - mSourceIdToEitMap.put(sourceId, items); - VctItem channel = mSourceIdToVctItemMap.get(sourceId); - if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) { - mVctItemHandledStatus.put(sourceId, true); - handleEitItems(channel, items); - } else { - mVctItemHandledStatus.put(sourceId, false); - if (!mIsDvbSignal) { - // Log only when zapping to non-DVB channels, since there is not VCT in DVB signal. - Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet."); - } - } - } - - /** - * Creates MPEG-2 TS parser. - * - * @param listener TsOutputListener - */ - public TsParser(TsOutputListener listener, boolean isDvbSignal) { - startListening(PAT_PID); - startListening(ATSC_SI_BASE_PID); - mIsDvbSignal = isDvbSignal; - if (isDvbSignal) { - startListening(DVB_EIT_PID); - startListening(DVB_SDT_PID); - } - mListener = listener; - } - - private void startListening(int pid) { - mStreamMap.put(pid, new SectionStream(pid)); - } - - private boolean feedTSPacket(byte[] tsData, int pos) { - if (tsData.length < pos + TS_PACKET_SIZE) { - if (DEBUG) Log.d(TAG, "Data should include a single TS packet."); - return false; - } - if (tsData[pos] != TS_PACKET_START_CODE) { - if (DEBUG) Log.d(TAG, "Invalid ts packet."); - return false; - } - if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) { - if (DEBUG) Log.d(TAG, "Erroneous ts packet."); - return false; - } - - // For details for the structure of TS packet, see H.222.0 Table 2-2. - int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff); - boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0; - boolean hasPayload = (tsData[pos + 3] & 0x10) != 0; - boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0; - int continuityCounter = tsData[pos + 3] & 0x0f; - Stream stream = mStreamMap.get(pid); - int payloadPos = pos; - payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4; - if (!hasPayload || stream == null) { - // We are not interested in this packet. - return false; - } - if (payloadPos >= pos + TS_PACKET_SIZE) { - if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet."); - return false; - } - stream.feedData(Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE), - continuityCounter, payloadStartIndicator); - return true; - } - - /** - * Feeds MPEG-2 TS data to parse. - * @param tsData buffer for ATSC TS stream - * @param pos the offset where buffer starts - * @param length The length of available data - */ - public void feedTSData(byte[] tsData, int pos, int length) { - for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) { - feedTSPacket(tsData, pos); - } - } - - /** - * Retrieves the channel information regardless of being well-formed. - * @return {@link List} of {@link TunerChannel} - */ - public List<TunerChannel> getMalFormedChannels() { - List<TunerChannel> incompleteChannels = new ArrayList<>(); - for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) { - if (!mProgramNumberHandledStatus.valueAt(i)) { - int programNumber = mProgramNumberHandledStatus.keyAt(i); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList); - incompleteChannels.add(tunerChannel); - } - } - } - return incompleteChannels; - } - - /** - * Reset the versions so that data with old version number can be handled. - */ - public void resetDataVersions() { - for (int eitPid : mEITPids) { - Stream stream = mStreamMap.get(eitPid); - if (stream != null) { - stream.resetDataVersions(); - } - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java deleted file mode 100644 index d2b4998a..00000000 --- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ /dev/null @@ -1,734 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.content.ComponentName; -import android.content.ContentProviderOperation; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.os.RemoteException; -import android.support.annotation.Nullable; -import android.text.format.DateUtils; -import android.util.Log; - -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.util.ConvertUtils; -import com.android.tv.util.PermissionUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Manages the channel info and EPG data through {@link TvInputManager}. - */ -public class ChannelDataManager implements Handler.Callback { - private static final String TAG = "ChannelDataManager"; - - private static final String[] ALL_PROGRAMS_SELECTION_ARGS = new String[] { - TvContract.Programs._ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_BROADCAST_GENRE, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_VERSION_NUMBER }; - private static final String[] CHANNEL_DATA_SELECTION_ARGS = new String[] { - TvContract.Channels._ID, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1}; - - private static final int MSG_HANDLE_EVENTS = 1; - private static final int MSG_HANDLE_CHANNEL = 2; - private static final int MSG_BUILD_CHANNEL_MAP = 3; - private static final int MSG_REQUEST_PROGRAMS = 4; - private static final int MSG_CLEAR_CHANNELS = 6; - private static final int MSG_CHECK_VERSION = 7; - - // Throttle the batch operations to avoid TransactionTooLargeException. - private static final int BATCH_OPERATION_COUNT = 100; - // At most 16 days of program information is delivered through an EIT, - // according to the Chapter 6.4 of ATSC Recommended Practice A/69. - private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16); - - /** - * A version number to enforce consistency of the channel data. - * - * WARNING: If a change in the database serialization lead to breaking the backward - * compatibility, you must increment this value so that the old data are purged, - * and the user is requested to perform the auto-scan again to generate the new data set. - */ - private static final int VERSION = 6; - - private final Context mContext; - private final String mInputId; - private ProgramInfoListener mListener; - private ChannelScanListener mChannelScanListener; - private Handler mChannelScanHandler; - private final HandlerThread mHandlerThread; - private final Handler mHandler; - private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap; - private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap; - private final Uri mChannelsUri; - - // Used for scanning - private final ConcurrentSkipListSet<TunerChannel> mScannedChannels; - private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels; - private final AtomicBoolean mIsScanning; - private final AtomicBoolean scanCompleted = new AtomicBoolean(); - - public interface ProgramInfoListener { - - /** - * Invoked when a request for getting programs of a channel has been processed and passes - * the requested channel and the programs retrieved from database to the listener. - */ - void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs); - - /** - * Invoked when programs of a channel have been arrived and passes the arrived channel and - * programs to the listener. - */ - void onProgramsArrived(TunerChannel channel, List<EitItem> programs); - - /** - * Invoked when a channel has been arrived and passes the arrived channel to the listener. - */ - void onChannelArrived(TunerChannel channel); - - /** - * Invoked when the database schema has been changed and the old-format channels have been - * deleted. A receiver should notify to a user that re-scanning channels is necessary. - */ - void onRescanNeeded(); - } - - public interface ChannelScanListener { - /** - * Invoked when all pending channels have been handled. - */ - void onChannelHandlingDone(); - } - - public ChannelDataManager(Context context) { - mContext = context; - mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(), - TunerTvInputService.class.getName())); - mChannelsUri = TvContract.buildChannelsUriForInput(mInputId); - mTunerChannelMap = new ConcurrentHashMap<>(); - mTunerChannelIdMap = new ConcurrentSkipListMap<>(); - mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread"); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper(), this); - mIsScanning = new AtomicBoolean(); - mScannedChannels = new ConcurrentSkipListSet<>(); - mPreviousScannedChannels = new ConcurrentSkipListSet<>(); - } - - // Public methods - public void checkDataVersion(Context context) { - int version = TunerPreferences.getChannelDataVersion(context); - Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")"); - if (version == VERSION) { - // Everything is awesome. Return and continue. - return; - } - setCurrentVersion(context); - - if (version == TunerPreferences.CHANNEL_DATA_VERSION_NOT_SET) { - mHandler.sendEmptyMessage(MSG_CHECK_VERSION); - } else { - // The stored channel data seem outdated. Delete them all. - mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS); - } - } - - public void setCurrentVersion(Context context) { - TunerPreferences.setChannelDataVersion(context, VERSION); - } - - public void setListener(ProgramInfoListener listener) { - mListener = listener; - } - - public void setChannelScanListener(ChannelScanListener listener, Handler handler) { - mChannelScanListener = listener; - mChannelScanHandler = handler; - } - - public void release() { - mHandler.removeCallbacksAndMessages(null); - releaseSafely(); - } - - public void releaseSafely() { - mHandlerThread.quitSafely(); - mListener = null; - mChannelScanListener = null; - mChannelScanHandler = null; - } - - public TunerChannel getChannel(long channelId) { - TunerChannel channel = mTunerChannelMap.get(channelId); - if (channel != null) { - return channel; - } - mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); - byte[] data = null; - try (Cursor cursor = mContext.getContentResolver().query(TvContract.buildChannelUri( - channelId), CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - data = cursor.getBlob(1); - } - } - if (data == null) { - return null; - } - channel = TunerChannel.parseFrom(data); - if (channel == null) { - return null; - } - channel.setChannelId(channelId); - return channel; - } - - public void requestProgramsData(TunerChannel channel) { - mHandler.removeMessages(MSG_REQUEST_PROGRAMS); - mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget(); - } - - public void notifyEventDetected(TunerChannel channel, List<EitItem> items) { - mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget(); - } - - public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (mIsScanning.get()) { - // During scanning, channels should be handle first to improve scan time. - // EIT items can be handled in background after channel scan. - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel)); - } else { - mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget(); - } - } - - // For scanning process - /** - * Invoked when starting a scanning mode. This method gets the previous channels to detect the - * obsolete channels after scanning and initializes the variables used for scanning. - */ - public void notifyScanStarted() { - mScannedChannels.clear(); - mPreviousScannedChannels.clear(); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long channelId = cursor.getLong(0); - byte[] data = cursor.getBlob(1); - TunerChannel channel = TunerChannel.parseFrom(data); - if (channel != null) { - channel.setChannelId(channelId); - mPreviousScannedChannels.add(channel); - } - } while (cursor.moveToNext()); - } - } - mIsScanning.set(true); - } - - /** - * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler - * in order to wait for finishing the remaining messages in the handler queue. Then removes the - * obsolete channels, which are previously scanned but are not in the current scanned result. - */ - public void notifyScanCompleted() { - // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue - // and avoid race conditions. - scanCompleted.set(true); - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null)); - } - - public void scannedChannelHandlingCompleted() { - mIsScanning.set(false); - if (!mPreviousScannedChannels.isEmpty()) { - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - for (TunerChannel channel : mPreviousScannedChannels) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildChannelUri(channel.getChannelId())).build()); - } - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error deleting obsolete channels", e); - } - } - if (mChannelScanListener != null && mChannelScanHandler != null) { - mChannelScanHandler.post(new Runnable() { - @Override - public void run() { - mChannelScanListener.onChannelHandlingDone(); - } - }); - } else { - Log.e(TAG, "Error. mChannelScanListener is null."); - } - } - - /** - * Returns the number of scanned channels in the scanning mode. - */ - public int getScannedChannelCount() { - return mScannedChannels.size(); - } - - /** - * Removes all callbacks and messages in handler to avoid previous messages from last channel. - */ - public void removeAllCallbacksAndMessages() { - mHandler.removeCallbacksAndMessages(null); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_HANDLE_EVENTS: { - ChannelEvent event = (ChannelEvent) msg.obj; - handleEvents(event.channel, event.eitItems); - return true; - } - case MSG_HANDLE_CHANNEL: { - TunerChannel channel = (TunerChannel) msg.obj; - if (channel != null) { - handleChannel(channel); - } - if (scanCompleted.get() && mIsScanning.get() - && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) { - // Complete the scan when all found channels have already been handled. - scannedChannelHandlingCompleted(); - } - return true; - } - case MSG_BUILD_CHANNEL_MAP: { - mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP); - buildChannelMap(); - return true; - } - case MSG_REQUEST_PROGRAMS: { - if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) { - return true; - } - TunerChannel channel = (TunerChannel) msg.obj; - if (mListener != null) { - mListener.onRequestProgramsResponse(channel, getAllProgramsForChannel(channel)); - } - return true; - } - case MSG_CLEAR_CHANNELS: { - clearChannels(); - return true; - } - case MSG_CHECK_VERSION: { - checkVersion(); - return true; - } - } - return false; - } - - // Private methods - private void handleEvents(TunerChannel channel, List<EitItem> items) { - long channelId = getChannelId(channel); - if (channelId <= 0) { - return; - } - channel.setChannelId(channelId); - - // Schedule the audio and caption tracks of the current program and the programs being - // listed after the current one into TIS. - if (mListener != null) { - mListener.onProgramsArrived(channel, items); - } - - long currentTime = System.currentTimeMillis(); - List<EitItem> oldItems = getAllProgramsForChannel(channel, currentTime, - currentTime + PROGRAM_QUERY_DURATION); - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - // TODO: Find a right way to check if the programs are added outside. - boolean addedOutside = false; - for (EitItem item : oldItems) { - if (item.getEventId() == 0) { - // The event has been added outside TV tuner. - addedOutside = true; - break; - } - } - - // Inserting programs only when there is no overlapping with existing data assuming that: - // 1. external EPG is more accurate and rich and - // 2. the data we add here will be updated when we apply external EPG. - if (addedOutside) { - // oldItemCount cannot be 0 if addedOutside is true. - int oldItemCount = oldItems.size(); - for (EitItem newItem : items) { - if (newItem.getEndTimeUtcMillis() < currentTime) { - continue; - } - long newItemStartTime = newItem.getStartTimeUtcMillis(); - long newItemEndTime = newItem.getEndTimeUtcMillis(); - if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) { - // Start time smaller than that of any old items. Insert if no overlap. - if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue; - } else if (newItemStartTime - > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) { - // Start time larger than that of any old item. Insert if no overlap. - if (newItemStartTime - < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis()) continue; - } else { - int pos = Collections.binarySearch(oldItems, newItem, - new Comparator<EitItem>() { - @Override - public int compare(EitItem lhs, EitItem rhs) { - return Long.compare(lhs.getStartTimeUtcMillis(), - rhs.getStartTimeUtcMillis()); - } - }); - if (pos >= 0) { - // Same start Time found. Overlapped. - continue; - } - int insertPoint = -1 - pos; - // Check the two adjacent items. - if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis() - || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) { - continue; - } - } - ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), newItem, channel)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - applyBatch(channel.getName(), ops); - return; - } - - List<EitItem> outdatedOldItems = new ArrayList<>(); - Map<Integer, EitItem> newEitItemMap = new HashMap<>(); - for (EitItem item : items) { - newEitItemMap.put(item.getEventId(), item); - } - for (EitItem oldItem : oldItems) { - EitItem item = newEitItemMap.get(oldItem.getEventId()); - if (item == null) { - outdatedOldItems.add(oldItem); - continue; - } - - // Since program descriptions arrive at different time, the older one may have the - // correct program description while the newer one has no clue what value is. - if (oldItem.getDescription() != null && item.getDescription() == null - && oldItem.getEventId() == item.getEventId() - && oldItem.getStartTime() == item.getStartTime() - && oldItem.getLengthInSecond() == item.getLengthInSecond() - && Objects.equals(oldItem.getContentRating(), item.getContentRating()) - && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre()) - && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) { - item.setDescription(oldItem.getDescription()); - } - if (item.compareTo(oldItem) != 0) { - ops.add(buildContentProviderOperation(ContentProviderOperation.newUpdate( - TvContract.buildProgramUri(oldItem.getProgramId())), item, null)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - newEitItemMap.remove(item.getEventId()); - } - for (EitItem unverifiedOldItems : outdatedOldItems) { - if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) { - // The given new EIT item list covers partial time span of EPG. Here, we delete old - // item only when it has an overlapping with the new EIT item list. - long startTime = unverifiedOldItems.getStartTimeUtcMillis(); - long endTime = unverifiedOldItems.getEndTimeUtcMillis(); - for (EitItem item : newEitItemMap.values()) { - long newItemStartTime = item.getStartTimeUtcMillis(); - long newItemEndTime = item.getEndTimeUtcMillis(); - if ((startTime >= newItemStartTime && startTime < newItemEndTime) - || (endTime > newItemStartTime && endTime <= newItemEndTime)) { - ops.add(ContentProviderOperation.newDelete(TvContract.buildProgramUri( - unverifiedOldItems.getProgramId())).build()); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - break; - } - } - } - } - for (EitItem item : newEitItemMap.values()) { - if (item.getEndTimeUtcMillis() < currentTime) { - continue; - } - ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), item, channel)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - - applyBatch(channel.getName(), ops); - } - - private ContentProviderOperation buildContentProviderOperation( - ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { - if (channel != null) { - builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED, - channel.isRecordingProhibited() ? 1 : 0); - } - } - if (item != null) { - builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) - .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - item.getStartTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - item.getEndTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, - item.getContentRating()) - .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, - item.getAudioLanguage()) - .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - item.getDescription()) - .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, - item.getEventId()); - } - return builder.build(); - } - - private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) { - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error updating EPG " + channelName, e); - } - } - - private void handleChannel(TunerChannel channel) { - long channelId = getChannelId(channel); - ContentValues values = new ContentValues(); - values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName()); - values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName()); - values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid()); - values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber()); - values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName()); - values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray()); - values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription()); - values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat()); - values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION); - values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, - channel.isRecordingProhibited() ? 1 : 0); - - if (channelId <= 0) { - values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId); - values.put(TvContract.Channels.COLUMN_TYPE, "QAM256".equals(channel.getModulation()) - ? TvContract.Channels.TYPE_ATSC_C : TvContract.Channels.TYPE_ATSC_T); - values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber()); - - // ATSC doesn't have original_network_id - values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency()); - - Uri channelUri = mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, - values); - channelId = ContentUris.parseId(channelUri); - } else { - mContext.getContentResolver().update( - TvContract.buildChannelUri(channelId), values, null, null); - } - channel.setChannelId(channelId); - mTunerChannelMap.put(channelId, channel); - mTunerChannelIdMap.put(channel, channelId); - if (mIsScanning.get()) { - mScannedChannels.add(channel); - mPreviousScannedChannels.remove(channel); - } - if (mListener != null) { - mListener.onChannelArrived(channel); - } - } - - private void clearChannels() { - int count = mContext.getContentResolver().delete(mChannelsUri, null, null); - if (count > 0) { - // We have just deleted obsolete data. Now tell the user that he or she needs - // to perform the auto-scan again. - if (mListener != null) { - mListener.onRescanNeeded(); - } - } - } - - private void checkVersion() { - if (PermissionUtils.hasAccessAllEpg(mContext)) { - String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?"; - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, selection, - new String[] {Integer.toString(VERSION)}, null)) { - if (cursor != null && cursor.moveToFirst()) { - // The stored channel data seem outdated. Delete them all. - clearChannels(); - } - } - } else { - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - new String[] { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 }, - null, null, null)) { - if (cursor != null) { - while (cursor.moveToNext()) { - int version = cursor.getInt(0); - if (version != VERSION) { - clearChannels(); - break; - } - } - } - } - } - } - - private long getChannelId(TunerChannel channel) { - Long channelId = mTunerChannelIdMap.get(channel); - if (channelId != null) { - return channelId; - } - mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - channelId = cursor.getLong(0); - byte[] providerData = cursor.getBlob(1); - TunerChannel tunerChannel = TunerChannel.parseFrom(providerData); - if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) { - channel.setChannelId(channelId); - mTunerChannelIdMap.put(channel, channelId); - mTunerChannelMap.put(channelId, channel); - return channelId; - } - } while (cursor.moveToNext()); - } - } - return -1; - } - - private List<EitItem> getAllProgramsForChannel(TunerChannel channel) { - return getAllProgramsForChannel(channel, null, null); - } - - private List<EitItem> getAllProgramsForChannel(TunerChannel channel, @Nullable Long startTimeMs, - @Nullable Long endTimeMs) { - List<EitItem> items = new ArrayList<>(); - long channelId = channel.getChannelId(); - Uri programsUri = (startTimeMs == null || endTimeMs == null) ? - TvContract.buildProgramsUriForChannel(channelId) : - TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs); - try (Cursor cursor = mContext.getContentResolver().query(programsUri, - ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long id = cursor.getLong(0); - String titleText = cursor.getString(1); - long startTime = ConvertUtils.convertUnixEpochToGPSTime( - cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS); - long endTime = ConvertUtils.convertUnixEpochToGPSTime( - cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS); - int lengthInSecond = (int) (endTime - startTime); - String contentRating = cursor.getString(4); - String broadcastGenre = cursor.getString(5); - String canonicalGenre = cursor.getString(6); - String description = cursor.getString(7); - int eventId = cursor.getInt(8); - EitItem eitItem = new EitItem(id, eventId, titleText, startTime, lengthInSecond, - contentRating, null, null, broadcastGenre, canonicalGenre, description); - items.add(eitItem); - } while (cursor.moveToNext()); - } - } - return items; - } - - private void buildChannelMap() { - ArrayList<TunerChannel> channels = new ArrayList<>(); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long channelId = cursor.getLong(0); - byte[] data = cursor.getBlob(1); - TunerChannel channel = TunerChannel.parseFrom(data); - if (channel != null) { - channel.setChannelId(channelId); - channels.add(channel); - } - } while (cursor.moveToNext()); - } - } - mTunerChannelMap.clear(); - mTunerChannelIdMap.clear(); - for (TunerChannel channel : channels) { - mTunerChannelMap.put(channel.getChannelId(), channel); - mTunerChannelIdMap.put(channel, channel.getChannelId()); - } - } - - private static class ChannelEvent { - public final TunerChannel channel; - public final List<EitItem> eitItems; - - public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) { - this.channel = channel; - this.eitItems = eitItems; - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java deleted file mode 100644 index dc99118a..00000000 --- a/src/com/android/tv/tuner/tvinput/EventDetector.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.data.PsiData; -import com.android.tv.tuner.data.PsipData; -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 java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Detects channels and programs that are emerged or changed while parsing ATSC PSIP information. - */ -public class EventDetector { - private static final String TAG = "EventDetector"; - private static final boolean DEBUG = false; - public static final int ALL_PROGRAM_NUMBERS = -1; - - private final TunerHal mTunerHal; - - private TsParser mTsParser; - private final Set<Integer> mPidSet = new HashSet<>(); - - // To prevent channel duplication - private final Set<Integer> mVctProgramNumberSet = new HashSet<>(); - private final Set<Integer> mSdtProgramNumberSet = new HashSet<>(); - private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>(); - private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); - private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final List<EventListener> mEventListeners = new ArrayList<>(); - private int mFrequency; - private String mModulation; - private int mProgramNumber = ALL_PROGRAM_NUMBERS; - - private final TsParser.TsOutputListener mTsOutputListener = new TsParser.TsOutputListener() { - @Override - public void onPatDetected(List<PsiData.PatItem> items) { - for (PsiData.PatItem i : items) { - if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { - mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER); - } - } - } - - @Override - public void onEitPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onEitItemParsed(PsipData.VctItem channel, List<PsipData.EitItem> items) { - TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber()); - if (DEBUG) { - Log.d(TAG, "onEitItemParsed tunerChannel:" + tunerChannel + " " - + channel.getProgramNumber()); - } - int channelSourceId = channel.getSourceId(); - - // Source id 0 is useful for cases where a cable operator wishes to define a channel for - // which no EPG data is currently available. - // We don't handle such a case. - if (channelSourceId == 0) { - return; - } - - // If at least a one caption track have been found in EIT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId); - for (PsipData.EitItem item : items) { - if (captionTracksFound) { - break; - } - List<AtscCaptionTrack> captionTracks = item.getCaptionTracks(); - if (captionTracks != null && !captionTracks.isEmpty()) { - captionTracksFound = true; - } - } - mEitCaptionTracksFound.put(channelSourceId, captionTracksFound); - if (captionTracksFound) { - for (PsipData.EitItem item : items) { - item.setHasCaptionTrack(); - } - } - if (tunerChannel != null && !mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onEventDetected(tunerChannel, items); - } - } - } - - @Override - public void onEttPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onAllVctItemsParsed() { - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelScanDone(); - } - } - } - - @Override - public void onVctItemParsed(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onVctItemParsed VCT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = new TunerChannel(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PsiData.PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getProgramNumber(); - - // If at least a one caption track have been found in VCT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mVctCaptionTracksFound.get(channelProgramNumber) - || !captionTracks.isEmpty(); - mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound); - if (captionTracksFound) { - tunerChannel.setHasCaptionTrack(); - } - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - tunerChannel.setFrequency(mFrequency); - tunerChannel.setModulation(mModulation); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mVctProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mVctProgramNumberSet.add(channelProgramNumber); - } - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelDetected(tunerChannel, !found); - } - } - } - - @Override - public void onSdtItemParsed(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onSdtItemParsed SDT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = new TunerChannel(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PsiData.PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getServiceId(); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - tunerChannel.setFrequency(mFrequency); - tunerChannel.setModulation(mModulation); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mSdtProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mSdtProgramNumberSet.add(channelProgramNumber); - } - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelDetected(tunerChannel, !found); - } - } - } - }; - - /** - * 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); - - /** - * Fired when new program events of an ATSC TV channel arrived. - * - * @param channel an ATSC TV channel - * @param items a list of EIT items that were received - */ - void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items); - - /** - * Fired when information of all detectable ATSC TV channels in current frequency arrived. - */ - void onChannelScanDone(); - } - - /** - * Creates a detector for ATSC TV channles and program information. - * - * @param usbTunerInteface {@link TunerHal} - */ - public EventDetector(TunerHal usbTunerInteface) { - mTunerHal = usbTunerInteface; - } - - private void reset() { - // TODO: Use TsParser.reset() - int deliverySystemType = mTunerHal.getDeliverySystemType(); - mTsParser = - new TsParser( - mTsOutputListener, - TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType())); - mPidSet.clear(); - mVctProgramNumberSet.clear(); - mSdtProgramNumberSet.clear(); - mVctCaptionTracksFound.clear(); - mEitCaptionTracksFound.clear(); - mChannelMap.clear(); - } - - /** - * Starts detecting channel and program information. - * - * @param frequency The frequency to listen to. - * @param modulation The modulation type. - * @param programNumber The program number if this is for handling tune request. For scanning - * purpose, supply {@link #ALL_PROGRAM_NUMBERS}. - */ - public void startDetecting(int frequency, String modulation, int programNumber) { - reset(); - mFrequency = frequency; - mModulation = modulation; - mProgramNumber = programNumber; - } - - private void startListening(int pid) { - if (mPidSet.contains(pid)) { - return; - } - mPidSet.add(pid); - mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER); - } - - /** - * Feeds ATSC TS stream to detect channel and program information. - * @param data buffer for ATSC TS stream - * @param startOffset the offset where buffer starts - * @param length The length of available data - */ - public void feedTSStream(byte[] data, int startOffset, int length) { - if (mPidSet.isEmpty()) { - startListening(TsParser.ATSC_SI_BASE_PID); - } - if (mTsParser != null) { - mTsParser.feedTSData(data, startOffset, length); - } - } - - /** - * Retrieves the channel information regardless of being well-formed. - * @return {@link List} of {@link TunerChannel} - */ - public List<TunerChannel> getMalFormedChannels() { - return mTsParser.getMalFormedChannels(); - } - - /** - * Registers an EventListener. - * @param eventListener the listener to be registered - */ - public void registerListener(EventListener eventListener) { - if (mTsParser != null) { - // Resets the version numbers so that the new listener can receive the EIT items. - // Otherwise, each EIT session is handled only once unless there is a new version. - mTsParser.resetDataVersions(); - } - mEventListeners.add(eventListener); - } - - /** - * Unregisters an EventListener. - * @param eventListener the listener to be unregistered - */ - public void unregisterListener(EventListener eventListener) { - boolean removed = mEventListeners.remove(eventListener); - if (!removed && DEBUG) { - Log.d(TAG, "Cannot unregister a non-registered listener!"); - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java deleted file mode 100644 index 99222bf8..00000000 --- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.tv.tuner.data.PsiData.PatItem; -import com.android.tv.tuner.data.PsiData.PmtItem; -import com.android.tv.tuner.data.PsipData.EitItem; -import com.android.tv.tuner.data.PsipData.SdtItem; -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.TsParser; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * PSIP event detector for a file source. - * - * <p>Uses {@link TsParser} to analyze input MPEG-2 transport stream, detects and reports - * various PSIP-related events via {@link TsParser.TsOutputListener}. - */ -public class FileSourceEventDetector { - private static final String TAG = "FileSourceEventDetector"; - private static final boolean DEBUG = true; - public static final int ALL_PROGRAM_NUMBERS = 0; - - private TsParser mTsParser; - private final Set<Integer> mVctProgramNumberSet = new HashSet<>(); - private final Set<Integer> mSdtProgramNumberSet = new HashSet<>(); - private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>(); - private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); - private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final EventListener mEventListener; - private final boolean mEnableDvbSignal; - private FileTsStreamer.StreamProvider mStreamProvider; - private int mProgramNumber = ALL_PROGRAM_NUMBERS; - - public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) { - mEventListener = listener; - mEnableDvbSignal = enableDvbSignal; - } - - /** - * Starts detecting channel and program information. - * - * @param provider MPEG-2 transport stream source. - * @param programNumber The program number if this is for handling tune request. For scanning - * purpose, supply {@link #ALL_PROGRAM_NUMBERS}. - */ - public void start(FileTsStreamer.StreamProvider provider, int programNumber) { - mStreamProvider = provider; - mProgramNumber = programNumber; - reset(); - } - - private void reset() { - mTsParser = new TsParser(mTsOutputListener, mEnableDvbSignal); // TODO: Use TsParser.reset() - mStreamProvider.clearPidFilter(); - mVctProgramNumberSet.clear(); - mSdtProgramNumberSet.clear(); - mVctCaptionTracksFound.clear(); - mEitCaptionTracksFound.clear(); - mChannelMap.clear(); - } - - public void feedTSStream(byte[] data, int startOffset, int length) { - if (mStreamProvider.isFilterEmpty()) { - startListening(TsParser.ATSC_SI_BASE_PID); - startListening(TsParser.PAT_PID); - } - if (mTsParser != null) { - mTsParser.feedTSData(data, startOffset, length); - } - } - - private void startListening(int pid) { - if (mStreamProvider.isInFilter(pid)) { - return; - } - mStreamProvider.addPidFilter(pid); - } - - private final TsParser.TsOutputListener mTsOutputListener = new TsParser.TsOutputListener() { - @Override - public void onPatDetected(List<PatItem> items) { - for (PatItem i : items) { - if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { - mStreamProvider.addPidFilter(i.getPmtPid()); - } - } - } - - @Override - public void onEitPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onEitItemParsed(VctItem channel, List<EitItem> items) { - TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber()); - if (DEBUG) { - Log.d(TAG, "onEitItemParsed tunerChannel:" + tunerChannel + " " - + channel.getProgramNumber()); - } - int channelSourceId = channel.getSourceId(); - - // Source id 0 is useful for cases where a cable operator wishes to define a channel for - // which no EPG data is currently available. - // We don't handle such a case. - if (channelSourceId == 0) { - return; - } - - // If at least a one caption track have been found in EIT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId); - for (EitItem item : items) { - if (captionTracksFound) { - break; - } - List<AtscCaptionTrack> captionTracks = item.getCaptionTracks(); - if (captionTracks != null && !captionTracks.isEmpty()) { - captionTracksFound = true; - } - } - mEitCaptionTracksFound.put(channelSourceId, captionTracksFound); - if (captionTracksFound) { - for (EitItem item : items) { - item.setHasCaptionTrack(); - } - } - if (tunerChannel != null && mEventListener != null) { - mEventListener.onEventDetected(tunerChannel, items); - } - } - - @Override - public void onEttPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onAllVctItemsParsed() { - // do nothing. - } - - @Override - public void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onVctItemParsed VCT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = TunerChannel.forFile(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getProgramNumber(); - - // If at least a one caption track have been found in VCT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mVctCaptionTracksFound.get(channelProgramNumber) - || !captionTracks.isEmpty(); - mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound); - if (captionTracksFound) { - tunerChannel.setHasCaptionTrack(); - } - tunerChannel.setFilepath(mStreamProvider.getFilepath()); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mVctProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mVctProgramNumberSet.add(channelProgramNumber); - } - if (mEventListener != null) { - mEventListener.onChannelDetected(tunerChannel, !found); - } - } - - @Override - public void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onSdtItemParsed SDT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = TunerChannel.forDvbFile(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getServiceId(); - tunerChannel.setFilepath(mStreamProvider.getFilepath()); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mSdtProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mSdtProgramNumberSet.add(channelProgramNumber); - } - if (mEventListener != null) { - mEventListener.onChannelDetected(tunerChannel, !found); - } - } - }; -} diff --git a/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java b/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java deleted file mode 100644 index 3908fe6c..00000000 --- a/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -/** - * The listener for buffer events occurred during playback. - */ -public interface PlaybackBufferListener { - - /** - * Invoked when the start position of the buffer has been changed. - * - * @param startTimeMs the new start time of the buffer in millisecond - */ - void onBufferStartTimeChanged(long startTimeMs); - - /** - * Invoked when the state of the buffer has been changed. - * - * @param available whether the buffer is available or not - */ - void onBufferStateChanged(boolean available); - - /** - * Invoked when the disk speed is too slow to write the buffers. - */ - void onDiskTooSlow(); -} diff --git a/src/com/android/tv/tuner/tvinput/TunerDebug.java b/src/com/android/tv/tuner/tvinput/TunerDebug.java deleted file mode 100644 index 2ddc946a..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerDebug.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.os.SystemClock; -import android.util.Log; - -/** - * A class to maintain various debugging information. - */ -public class TunerDebug { - private static final String TAG = "TunerDebug"; - public static final boolean ENABLED = false; - - private int mVideoFrameDrop; - private int mBytesInQueue; - - private long mAudioPositionUs; - private long mAudioPtsUs; - private long mVideoPtsUs; - - private long mLastAudioPositionUs; - private long mLastAudioPtsUs; - private long mLastVideoPtsUs; - private long mLastCheckTimestampMs; - - private long mAudioPositionUsRate; - private long mAudioPtsUsRate; - private long mVideoPtsUsRate; - - private TunerDebug() { - mVideoFrameDrop = 0; - mLastCheckTimestampMs = SystemClock.elapsedRealtime(); - } - - private static class LazyHolder { - private static final TunerDebug INSTANCE = new TunerDebug(); - } - - public static TunerDebug getInstance() { - return LazyHolder.INSTANCE; - } - - public static void notifyVideoFrameDrop(int count, long delta) { - // TODO: provide timestamp mismatch information using delta - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mVideoFrameDrop += count; - } - - public static int getVideoFrameDrop() { - TunerDebug sTunerDebug = getInstance(); - int videoFrameDrop = sTunerDebug.mVideoFrameDrop; - if (videoFrameDrop > 0) { - Log.d(TAG, "Dropped video frame: " + videoFrameDrop); - } - sTunerDebug.mVideoFrameDrop = 0; - return videoFrameDrop; - } - - public static void setBytesInQueue(int bytesInQueue) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mBytesInQueue = bytesInQueue; - } - - public static int getBytesInQueue() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mBytesInQueue; - } - - public static void setAudioPositionUs(long audioPositionUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mAudioPositionUs = audioPositionUs; - } - - public static long getAudioPositionUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPositionUs; - } - - public static void setAudioPtsUs(long audioPtsUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mAudioPtsUs = audioPtsUs; - } - - public static long getAudioPtsUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPtsUs; - } - - public static void setVideoPtsUs(long videoPtsUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mVideoPtsUs = videoPtsUs; - } - - public static long getVideoPtsUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mVideoPtsUs; - } - - public static void calculateDiff() { - TunerDebug sTunerDebug = getInstance(); - long currentTime = SystemClock.elapsedRealtime(); - long duration = currentTime - sTunerDebug.mLastCheckTimestampMs; - if (duration != 0) { - sTunerDebug.mAudioPositionUsRate = - (sTunerDebug.mAudioPositionUs - sTunerDebug.mLastAudioPositionUs) * 1000 - / duration; - sTunerDebug.mAudioPtsUsRate = - (sTunerDebug.mAudioPtsUs - sTunerDebug.mLastAudioPtsUs) * 1000 - / duration; - sTunerDebug.mVideoPtsUsRate = - (sTunerDebug.mVideoPtsUs - sTunerDebug.mLastVideoPtsUs) * 1000 - / duration; - } - - sTunerDebug.mLastAudioPositionUs = sTunerDebug.mAudioPositionUs; - sTunerDebug.mLastAudioPtsUs = sTunerDebug.mAudioPtsUs; - sTunerDebug.mLastVideoPtsUs = sTunerDebug.mVideoPtsUs; - sTunerDebug.mLastCheckTimestampMs = currentTime; - } - - public static long getAudioPositionUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPositionUsRate; - } - - public static long getAudioPtsUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPtsUsRate; - } - - public static long getVideoPtsUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mVideoPtsUsRate; - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java deleted file mode 100644 index acdd149f..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.content.Context; -import android.media.tv.TvInputManager; -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; - -/** - * Processes DVR recordings, and deletes the previously recorded contents. - */ -public class TunerRecordingSession extends TvInputService.RecordingSession { - private static final String TAG = "TunerRecordingSession"; - private static final boolean DEBUG = false; - - private final TunerRecordingSessionWorker mSessionWorker; - - public TunerRecordingSession(Context context, String inputId, - ChannelDataManager channelDataManager) { - super(context); - mSessionWorker = new TunerRecordingSessionWorker(context, inputId, channelDataManager, - this); - } - - // RecordingSession - @MainThread - @Override - public void onTune(Uri channelUri) { - // TODO(dvr): support calling more than once, http://b/27171225 - if (DEBUG) { - Log.d(TAG, "Requesting recording session tune: " + channelUri); - } - mSessionWorker.tune(channelUri); - } - - @MainThread - @Override - public void onRelease() { - if (DEBUG) { - Log.d(TAG, "Requesting recording session release."); - } - mSessionWorker.release(); - } - - @MainThread - @Override - public void onStartRecording(@Nullable Uri programUri) { - if (DEBUG) { - Log.d(TAG, "Requesting start recording."); - } - mSessionWorker.startRecording(programUri); - } - - @MainThread - @Override - public void onStopRecording() { - if (DEBUG) { - Log.d(TAG, "Requesting stop recording."); - } - mSessionWorker.stopRecording(); - } - - // Called from TunerRecordingSessionImpl in a worker thread. - @WorkerThread - public void onTuned(Uri channelUri) { - if (DEBUG) { - Log.d(TAG, "Notifying recording session tuned."); - } - notifyTuned(channelUri); - } - - @WorkerThread - public void onRecordFinished(final Uri recordedProgramUri) { - if (DEBUG) { - Log.d(TAG, "Notifying record successfully finished."); - } - notifyRecordingStopped(recordedProgramUri); - } - - @WorkerThread - public void onError(int reason) { - Log.w(TAG, "Notifying recording error: " + reason); - notifyError(reason); - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java deleted file mode 100644 index 34013bf1..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.os.AsyncTask; -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.util.Log; - -import android.util.Pair; -import com.google.android.exoplayer.C; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.recording.RecordingCapability; -import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.dvr.data.RecordedProgram; -import com.android.tv.tuner.DvbDeviceAccessor; -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.AtscCaptionTrack; -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.source.TsDataSource; -import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.util.Utils; - -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.List; -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.TimeUnit; - -/** - * Implements a DVR feature. - */ -public class TunerRecordingSessionWorker implements PlaybackBufferListener, - EventDetector.EventListener, SampleExtractor.OnCompletionListener, - Handler.Callback { - private static final String TAG = "TunerRecordingSessionW"; - private static final boolean DEBUG = false; - - private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS - + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " - + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; - private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); - private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); - private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); - private static final long PREPARE_RECORDER_POLL_MS = 50; - private static final int MSG_TUNE = 1; - private static final int MSG_START_RECORDING = 2; - private static final int MSG_PREPARE_RECODER = 3; - private static final int MSG_STOP_RECORDING = 4; - 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 final RecordingCapability mCapabilities; - - public RecordingCapability getCapabilities() { - return mCapabilities; - } - - @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING}) - @Retention(RetentionPolicy.SOURCE) - public @interface DvrSessionState {} - private static final int STATE_IDLE = 1; - private static final int STATE_TUNING = 2; - private static final int STATE_TUNED = 3; - private static final int STATE_RECORDING = 4; - - private static final long CHANNEL_ID_NONE = -1; - private static final int MAX_TUNING_RETRY = 6; - - private final Context mContext; - private final ChannelDataManager mChannelDataManager; - private final DvrStorageStatusManager mDvrStorageStatusManager; - private final Handler mHandler; - private final TsDataSourceManager mSourceManager; - private final Random mRandom = new Random(); - - private TsDataSource mTunerSource; - private TunerChannel mChannel; - private File mStorageDir; - private long mRecordStartTime; - private long mRecordEndTime; - private boolean mRecorderRunning; - private SampleExtractor mRecorder; - private final TunerRecordingSession mSession; - @DvrSessionState private int mSessionState = STATE_IDLE; - private final String mInputId; - private Uri mProgramUri; - - private PsipData.EitItem mCurrenProgram; - private List<AtscCaptionTrack> mCaptionTracks; - private DvrStorageManager mDvrStorageManager; - - public TunerRecordingSessionWorker(Context context, String inputId, - ChannelDataManager dataManager, TunerRecordingSession session) { - mRandom.setSeed(System.nanoTime()); - mContext = context; - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mDvrStorageStatusManager = - TvApplication.getSingletons(context).getDvrStorageStatusManager(); - mChannelDataManager = dataManager; - mChannelDataManager.checkDataVersion(context); - mSourceManager = TsDataSourceManager.createSourceManager(true); - mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); - mInputId = inputId; - if (DEBUG) Log.d(TAG, mCapabilities.toString()); - mSession = session; - } - - // PlaybackBufferListener - @Override - public void onBufferStartTimeChanged(long startTimeMs) { } - - @Override - public void onBufferStateChanged(boolean available) { } - - @Override - public void onDiskTooSlow() { } - - // EventDetector.EventListener - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (mChannel == null || mChannel.compareTo(channel) != 0) { - return; - } - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - - @Override - public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { - if (mChannel == null || mChannel.compareTo(channel) != 0) { - return; - } - mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget(); - mChannelDataManager.notifyEventDetected(channel, items); - } - - @Override - public void onChannelScanDone() { - // do nothing. - } - - // SampleExtractor.OnCompletionListener - @Override - public void onCompletion(boolean success, long lastExtractedPositionUs) { - onRecordingResult(success, lastExtractedPositionUs); - reset(); - } - - /** - * Tunes to {@code channelUri}. - */ - @MainThread - public void tune(Uri channelUri) { - mHandler.removeCallbacksAndMessages(null); - mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); - } - - /** - * Starts recording. - */ - @MainThread - public void startRecording(@Nullable Uri programUri) { - mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget(); - } - - /** - * Stops recording. - */ - @MainThread - public void stopRecording() { - mHandler.sendEmptyMessage(MSG_STOP_RECORDING); - } - - /** - * Releases all resources. - */ - @MainThread - public void release() { - mHandler.removeCallbacksAndMessages(null); - mHandler.sendEmptyMessage(MSG_RELEASE); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_TUNE: { - Uri channelUri = (Uri) msg.obj; - int retryCount = msg.arg1; - if (DEBUG) Log.d(TAG, "Tune to " + channelUri); - if (doTune(channelUri)) { - if (mSessionState == STATE_TUNED) { - mSession.onTuned(channelUri); - } else { - Log.w(TAG, "Tuner stream cannot be created due to resource shortage."); - if (retryCount < MAX_TUNING_RETRY) { - Message tuneMsg = - mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri); - mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS); - } else { - mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY); - reset(); - } - } - } - return true; - } - case MSG_START_RECORDING: { - if (DEBUG) Log.d(TAG, "Start recording"); - if (!doStartRecording((Uri) msg.obj)) { - reset(); - } - return true; - } - case MSG_PREPARE_RECODER: { - if (DEBUG) Log.d(TAG, "Preparing recorder"); - if (!mRecorderRunning) { - return true; - } - try { - if (!mRecorder.prepare()) { - mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER, - PREPARE_RECORDER_POLL_MS); - } - } catch (IOException e) { - Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor"); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - reset(); - } - return true; - } - case MSG_STOP_RECORDING: { - if (DEBUG) Log.d(TAG, "Stop recording"); - if (mSessionState != STATE_RECORDING) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - reset(); - return true; - } - if (mRecorderRunning) { - stopRecorder(); - } - return true; - } - case MSG_MONITOR_STORAGE_STATUS: { - if (mSessionState != STATE_RECORDING) { - return true; - } - if (!mDvrStorageStatusManager.isStorageSufficient()) { - if (mRecorderRunning) { - stopRecorder(); - } - new DeleteRecordingTask().execute(mStorageDir); - mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - reset(); - } else { - mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, - STORAGE_MONITOR_INTERVAL_MS); - } - return true; - } - case MSG_RELEASE: { - // Since release was requested, current recording will be cancelled - // without notification. - reset(); - mSourceManager.release(); - mHandler.removeCallbacksAndMessages(null); - mHandler.getLooper().quitSafely(); - return true; - } - case MSG_UPDATE_CC_INFO: { - Pair<TunerChannel, List<EitItem>> pair = - (Pair<TunerChannel, List<EitItem>>) msg.obj; - updateCaptionTracks(pair.first, pair.second); - return true; - } - } - return false; - } - - @Nullable - private TunerChannel getChannel(Uri channelUri) { - if (channelUri == null) { - return null; - } - long channelId; - try { - channelId = ContentUris.parseId(channelUri); - } catch (UnsupportedOperationException | NumberFormatException e) { - channelId = CHANNEL_ID_NONE; - } - return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); - } - - private String getStorageKey() { - long prefix = System.currentTimeMillis(); - int suffix = mRandom.nextInt(); - return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); - } - - private void reset() { - if (mRecorder != null) { - mRecorder.release(); - mRecorder = null; - } - if (mTunerSource != null) { - mSourceManager.releaseDataSource(mTunerSource); - mTunerSource = null; - } - mDvrStorageManager = null; - mSessionState = STATE_IDLE; - mRecorderRunning = false; - } - - private boolean doTune(Uri channelUri) { - if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.e(TAG, "Tuning was requested from wrong status."); - return false; - } - mChannel = getChannel(channelUri); - if (mChannel == null) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); - return false; - } else if (mChannel.isRecordingProhibited()) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel); - return false; - } - if (!mDvrStorageStatusManager.isStorageSufficient()) { - mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Log.w(TAG, "Tuning failed due to insufficient storage."); - return false; - } - mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); - if (mTunerSource == null) { - // Retry tuning in this case. - mSessionState = STATE_TUNING; - return true; - } - mSessionState = STATE_TUNED; - return true; - } - - private boolean doStartRecording(@Nullable Uri programUri) { - if (mSessionState != STATE_TUNED) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.e(TAG, "Recording session status abnormal"); - return false; - } - mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ? - new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(), - getStorageKey()) : null; - if (mStorageDir == null) { - mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Log.w(TAG, "Failed to start recording due to insufficient storage."); - return false; - } - // Since tuning might be happened a while ago, shifts the start position of tuned source. - mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); - mRecordStartTime = System.currentTimeMillis(); - mDvrStorageManager = new DvrStorageManager(mStorageDir, true); - mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, - new BufferManager(mDvrStorageManager), this, true); - mRecorder.setOnCompletionListener(this, mHandler); - mProgramUri = programUri; - mSessionState = STATE_RECORDING; - mRecorderRunning = true; - mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); - mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); - mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, - STORAGE_MONITOR_INTERVAL_MS); - return true; - } - - private void stopRecorder() { - // Do not change session status. - if (mRecorder != null) { - mRecorder.release(); - mRecordEndTime = System.currentTimeMillis(); - mRecorder = null; - } - mRecorderRunning = false; - mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); - Log.i(TAG, "Recording stopped"); - } - - private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) { - if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0 - || items == null || items.isEmpty()) { - return; - } - PsipData.EitItem currentProgram = getCurrentProgram(items); - if (currentProgram == null || !currentProgram.hasCaptionTrack() - || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) { - return; - } - mCurrenProgram = currentProgram; - mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks()); - if (DEBUG) { - Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for " - + currentProgram); - } - } - - private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) { - for (PsipData.EitItem item : items) { - if (mRecordStartTime >= item.getStartTimeUtcMillis() - && mRecordStartTime < item.getEndTimeUtcMillis()) { - return item; - } - } - return null; - } - - private static class Program { - private final long mChannelId; - private final String mTitle; - private String mSeriesId; - private final String mSeasonTitle; - private final String mEpisodeTitle; - private final String mSeasonNumber; - private final String mEpisodeNumber; - private final String mDescription; - private final String mPosterArtUri; - private final String mThumbnailUri; - private final String mCanonicalGenres; - private final String mContentRatings; - private final long mStartTimeUtcMillis; - private final long mEndTimeUtcMillis; - private final int mVideoWidth; - private final int mVideoHeight; - private final byte[] mInternalProviderData; - - private static final String[] PROJECTION = { - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_SEASON_TITLE, - TvContract.Programs.COLUMN_EPISODE_TITLE, - TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_POSTER_ART_URI, - TvContract.Programs.COLUMN_THUMBNAIL_URI, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_VIDEO_WIDTH, - TvContract.Programs.COLUMN_VIDEO_HEIGHT, - TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA - }; - - public Program(Cursor cursor) { - int index = 0; - mChannelId = cursor.getLong(index++); - mTitle = cursor.getString(index++); - mSeasonTitle = cursor.getString(index++); - mEpisodeTitle = cursor.getString(index++); - mSeasonNumber = cursor.getString(index++); - mEpisodeNumber = cursor.getString(index++); - mDescription = cursor.getString(index++); - mPosterArtUri = cursor.getString(index++); - mThumbnailUri = cursor.getString(index++); - mCanonicalGenres = cursor.getString(index++); - mContentRatings = cursor.getString(index++); - mStartTimeUtcMillis = cursor.getLong(index++); - mEndTimeUtcMillis = cursor.getLong(index++); - mVideoWidth = cursor.getInt(index++); - mVideoHeight = cursor.getInt(index++); - mInternalProviderData = cursor.getBlob(index++); - SoftPreconditions.checkArgument(index == PROJECTION.length); - } - - public Program(long channelId) { - mChannelId = channelId; - mTitle = "Unknown"; - mSeasonTitle = ""; - mEpisodeTitle = ""; - mSeasonNumber = ""; - mEpisodeNumber = ""; - mDescription = "Unknown"; - mPosterArtUri = null; - mThumbnailUri = null; - mCanonicalGenres = null; - mContentRatings = null; - mStartTimeUtcMillis = 0; - mEndTimeUtcMillis = 0; - mVideoWidth = 0; - mVideoHeight = 0; - mInternalProviderData = null; - } - - public static Program onQuery(Cursor c) { - Program program = null; - if (c != null && c.moveToNext()) { - program = new Program(c); - } - return program; - } - - public ContentValues buildValues() { - ContentValues values = new ContentValues(); - int index = 0; - values.put(PROJECTION[index++], mChannelId); - values.put(PROJECTION[index++], mTitle); - values.put(PROJECTION[index++], mSeasonTitle); - values.put(PROJECTION[index++], mEpisodeTitle); - values.put(PROJECTION[index++], mSeasonNumber); - values.put(PROJECTION[index++], mEpisodeNumber); - values.put(PROJECTION[index++], mDescription); - values.put(PROJECTION[index++], mPosterArtUri); - values.put(PROJECTION[index++], mThumbnailUri); - values.put(PROJECTION[index++], mCanonicalGenres); - values.put(PROJECTION[index++], mContentRatings); - values.put(PROJECTION[index++], mStartTimeUtcMillis); - values.put(PROJECTION[index++], mEndTimeUtcMillis); - values.put(PROJECTION[index++], mVideoWidth); - values.put(PROJECTION[index++], mVideoHeight); - values.put(PROJECTION[index++], mInternalProviderData); - SoftPreconditions.checkArgument(index == PROJECTION.length); - return values; - } - } - - private Program getRecordedProgram() { - ContentResolver resolver = mContext.getContentResolver(); - Uri programUri = mProgramUri; - if (mProgramUri == null) { - 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)) { - if (c != null) { - Program result = Program.onQuery(c); - if (DEBUG) { - Log.v(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 Uri insertRecordedProgram(Program program, long channelId, String storageUri, - long totalBytes, long startTime, long endTime) { - // TODO: Set title even though program is null. - RecordedProgram recordedProgram = RecordedProgram.builder() - .setInputId(mInputId) - .setChannelId(channelId) - .setDataUri(storageUri) - .setDurationMillis(endTime - startTime) - .setDataBytes(totalBytes) - // startTime and endTime could be overridden by program's start and end value. - .setStartTimeUtcMillis(startTime) - .setEndTimeUtcMillis(endTime) - .build(); - ContentValues values = RecordedProgram.toValues(recordedProgram); - if (program != null) { - values.putAll(program.buildValues()); - } - return mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI, - values); - } - - private void onRecordingResult(boolean success, long lastExtractedPositionUs) { - if (mSessionState != STATE_RECORDING) { - // Error notification is not needed. - Log.e(TAG, "Recording session status abnormal"); - return; - } - if (mRecorderRunning) { - // In case of recorder not being stopped, because of premature termination of recording. - stopRecorder(); - } - if (!success && lastExtractedPositionUs < - TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { - new DeleteRecordingTask().execute(mStorageDir); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.w(TAG, "Recording failed during recording"); - return; - } - Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); - long recordEndTime = - (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; - } - mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks); - mSession.onRecordFinished(uri); - } - - private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { - - @Override - public Void doInBackground(File... files) { - if (files == null || files.length == 0) { - return null; - } - for(File file : files) { - Utils.deleteDirOrFile(file); - } - return null; - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java deleted file mode 100644 index 44bae908..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerSession.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.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.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.google.android.exoplayer.audio.AudioCapabilities; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.TunerPreferences.TunerPreferencesChangedListener; -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.android.tv.tuner.util.SystemPropertiesProxy; - -/** - * Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions - * are implemented in {@link TunerSessionWorker}. - */ -public class TunerSession extends TvInputService.Session implements - Handler.Callback, TunerPreferencesChangedListener { - 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 TunerSessionWorker mSessionWorker; - private boolean mReleased = false; - private boolean mPlayPaused; - private long mTuneStartTimestamp; - - public TunerSession(Context context, ChannelDataManager channelDataManager) { - 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); - TunerPreferences.setTunerPreferencesChangedListener(this); - } - - public boolean isReleased() { - return mReleased; - } - - @Override - public View onCreateOverlayView() { - return mOverlayView; - } - - @Override - public boolean onSelectTrack(int type, String trackId) { - mSessionWorker.sendMessage(TunerSessionWorker.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(TunerSessionWorker.MSG_TIMESHIFT_PAUSE); - mPlayPaused = true; - } - - @Override - public void onTimeShiftResume() { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_RESUME); - mPlayPaused = false; - } - - @Override - public void onTimeShiftSeekTo(long timeMs) { - if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000); - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_SEEK_TO, - mPlayPaused ? 1 : 0, 0, timeMs); - } - - @Override - public void onTimeShiftSetPlaybackParams(PlaybackParams params) { - mSessionWorker.sendMessage( - TunerSessionWorker.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(TunerSessionWorker.MSG_UNBLOCKED_RATING, - unblockedRating); - } - - @Override - public void onRelease() { - if (DEBUG) { - Log.d(TAG, "onRelease"); - } - mReleased = true; - mSessionWorker.release(); - mUiHandler.removeCallbacksAndMessages(null); - TunerPreferences.setTunerPreferencesChangedListener(null); - } - - /** - * Sets {@link AudioCapabilities}. - */ - public void setAudioCapabilities(AudioCapabilities audioCapabilities) { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, - audioCapabilities); - } - - @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); - } - } - - 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 onTunerPreferencesChanged() { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED); - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java deleted file mode 100644 index e7eb017e..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ /dev/null @@ -1,1754 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.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.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.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.ExoPlayer; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvContentRatingCache; -import com.android.tv.customization.TvCustomizationManager; -import com.android.tv.customization.TvCustomizationManager.TRICKPLAY_MODE; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.TunerPreferences.TrickplaySetting; -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.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.MpegTsPlayer; -import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; -import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient; -import com.android.tv.tuner.source.TsDataSource; -import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.tuner.util.StatusTextUtils; -import com.android.tv.tuner.util.SystemPropertiesProxy; - -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; - -/** - * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs - * such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on. - */ -@WorkerThread -public class TunerSessionWorker implements PlaybackBufferListener, - MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener, - ChannelDataManager.ProgramInfoListener, Handler.Callback { - private static final String TAG = "TunerSessionWorker"; - 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_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; - private static final int MSG_RELEASE = 1001; - private 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; - private 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; - - 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; - // 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); - - // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker - // creation/release is required. - // This is used to guarantee that at most one active TunerSessionWorker exists at any give time. - 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 AudioCapabilities mAudioCapabilities; - private long mLastLimitInBytes; - private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); - private final TunerSession mSession; - 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(); - - public TunerSessionWorker(Context context, ChannelDataManager channelDataManager, - TunerSession tunerSession) { - 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); - mSession = tunerSession; - mChannelDataManager = channelDataManager; - mChannelDataManager.setListener(this); - mChannelDataManager.checkDataVersion(mContext); - mSourceManager = TsDataSourceManager.createSourceManager(false); - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - mTvTracks = new ArrayList<>(); - 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 = TvCustomizationManager.getTrickplayMode(context); - if (mTrickplayModeCustomization == - TvCustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { - boolean useExternalStorage = - Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && - Environment.isExternalStorageRemovable(); - mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null; - } else if (mTrickplayModeCustomization == TvCustomizationManager.TRICKPLAY_MODE_ENABLED) { - mTrickplayBufferDir = context.getCacheDir(); - } else { - mTrickplayBufferDir = null; - } - mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null; - mTrickplaySetting = TunerPreferences.getTrickplaySetting(context); - if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET - && mTrickplayModeCustomization - == TvCustomizationManager.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; - // NOTE: We assume that TunerSessionWorker instance will be at most one. - // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time. - // connect() will return false, if there is a connected TunerSessionWorker already. - mHasSoftwareAudioDecoder = FfmpegDecoderClient.connect(context); - } - - // 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) { - FfmpegDecoderClient.disconnect(mContext); - } - 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"); - mSession.sendUiMessage( - TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE); - } - - // MpegTsPlayer.VideoEventListener - @Override - public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { - mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event); - } - - @Override - public void onClearCaptionEvent() { - mSession.sendUiMessage(TunerSession.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() { - mSession.sendUiMessage(TunerSession.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 static final String[] PROJECTION = { - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, - }; - - public RecordedProgram(Cursor cursor) { - int index = 0; - mChannelId = cursor.getLong(index++); - mDataUri = cursor.getString(index++); - } - - public RecordedProgram(long channelId, String dataUri) { - mChannelId = channelId; - mDataUri = dataUri; - } - - 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; - } - } - - 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) { - return recording.getDataUri(); - } - return null; - } - - @Override - 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) { - 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; - } - 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; - } - 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; - } - 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; - } - case MSG_RESET_PLAYBACK: { - if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK"); - mChannelDataManager.removeAllCallbacksAndMessages(); - resetPlayback(); - return true; - } - case MSG_START_PLAYBACK: { - if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK"); - if (mChannel != null || mRecordingId != null) { - startPlayback((int) msg.obj); - } - return true; - } - case MSG_UPDATE_PROGRAM: { - if (mChannel != null) { - EitItem program = (EitItem) msg.obj; - updateTvTracks(program, false); - mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); - } - return true; - } - 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; - } - case MSG_UPDATE_CHANNEL_INFO: { - TunerChannel channel = (TunerChannel) msg.obj; - if (mChannel != null && mChannel.compareTo(channel) == 0) { - updateChannelInfo(channel); - } - return true; - } - 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; - } - case MSG_TRICKPLAY_BY_SEEK: { - if (mPlayer == null) { - return true; - } - doTrickplayBySeek(msg.arg1); - return true; - } - 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; - } - case MSG_RESCHEDULE_PROGRAMS: { - if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { - mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); - } else { - doReschedulePrograms(); - } - return true; - } - case MSG_PARENTAL_CONTROLS: { - doParentalControls(); - mHandler.removeMessages(MSG_PARENTAL_CONTROLS); - mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, - PARENTAL_CONTROLS_INTERVAL_MS); - return true; - } - 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; - } - case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: { - int serviceNumber = (int) msg.obj; - doDiscoverCaptionServiceNumber(serviceNumber); - return true; - } - case MSG_SELECT_TRACK: { - if (mChannel != null || mRecordingId != null) { - doSelectTrack(msg.arg1, (String) msg.obj); - } - return true; - } - case MSG_UPDATE_CAPTION_TRACK: { - if (mCaptionEnabled) { - startCaptionTrack(); - } else { - stopCaptionTrack(); - } - return true; - } - case MSG_TIMESHIFT_PAUSE: { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftPause(); - return true; - } - case MSG_TIMESHIFT_RESUME: { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftResume(); - return true; - } - 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; - } - 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; - } - case MSG_SET_STREAM_VOLUME: { - if (mPlayer != null && mPlayer.isPlaying()) { - mPlayer.setVolume(mVolume); - } - return true; - } - 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; - } - 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; - } - 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; - } - case MSG_CHECK_SIGNAL: { - if (mChannel == null || mPlayer == null) { - 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() - ))); - } - 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.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); - 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); - } - } - mLastLimitInBytes = limitInBytes; - mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); - 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; - } - case MSG_NOTIFY_AUDIO_TRACK_UPDATED: { - notifyAudioTracksUpdated(); - return true; - } - default: { - 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 != TvCustomizationManager.TRICKPLAY_MODE_ENABLED) { - return; - } - if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { - mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED; - TunerPreferences.setTrickplaySetting( - mContext, mTrickplaySetting); - } - } - - private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { - if (capabilities == null) { - Log.w(TAG, "No Audio Capabilities"); - } - long now = System.currentTimeMillis(); - if (mTrickplayModeCustomization == TvCustomizationManager.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), - mHandler, mSourceManager, capabilities, this); - Log.i(TAG, "Passthrough AC3 renderer"); - if (DEBUG) Log.d(TAG, "ExoPlayer created"); - return player; - } - - private void startCaptionTrack() { - if (mCaptionEnabled && mCaptionTrack != null) { - mSession.sendUiMessage( - TunerSession.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); - } - mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK); - } - - private void resetTvTracks() { - mTvTracks.clear(); - mAudioTrackMap.clear(); - mCaptionTrackMap.clear(); - mSession.sendUiMessage(TunerSession.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; - mSession.sendUiMessage(TunerSession.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.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); - } - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); - mPlayerStarted = true; - } - } - - private void preparePlayback() { - SoftPreconditions.checkState(mPlayer == null); - if (mChannel == null && mRecordingId == null) { - return; - } - mSourceManager.setKeepTuneStatus(true); - 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(); - 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); - } - } - - private void resetPlayback() { - long timestamp, 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) { - return; - } - 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; - 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); - } - - 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.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; - } - TvContentRating[] ratings = mTvContentRatingCache - .getRatings(currentProgram.getContentRating()); - if (ratings == null || ratings.length == 0) { - ratings = new TvContentRating[] {TvContentRating.UNRATED}; - } - 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() { - // If MSG_RELEASE is removed, TunerSessionWorker will hang forever. - // Do not remove messages, after release is requested from MainThread. - synchronized (mReleaseLock) { - if (!mReleaseRequested) { - 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/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java deleted file mode 100644 index 6ad00daa..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java +++ /dev/null @@ -1,174 +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.tvinput; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; - -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.util.Utils; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Creates {@link JobService} to clean up recorded program files which are not referenced - * from database. - */ -public class TunerStorageCleanUpService extends JobService { - private static final String TAG = "TunerStorageCleanUpService"; - - private CleanUpStorageTask mTask; - - @Override - public void onCreate() { - if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) { - Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); - this.stopSelf(); - return; - } - TvApplication.setCurrentRunningProcess(this, false); - super.onCreate(); - mTask = new CleanUpStorageTask(this, this); - } - - @Override - public boolean onStartJob(JobParameters params) { - mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - return false; - } - - /** - * Cleans up recorded program files which are not referenced from database. - * Cleaning up will be done periodically. - */ - public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> { - private final static String[] mProjection = { - TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, - TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI - }; - private final static long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1); - - private final Context mContext; - private final DvrStorageStatusManager mDvrStorageStatusManager; - private final JobService mJobService; - private final ContentResolver mContentResolver; - - /** - * Creates a recurring storage cleaning task. - * - * @param context {@link Context} - * @param jobService {@link JobService} - */ - public CleanUpStorageTask(Context context, JobService jobService) { - mContext = context; - mDvrStorageStatusManager = - TvApplication.getSingletons(mContext).getDvrStorageStatusManager(); - mJobService = jobService; - mContentResolver = mContext.getContentResolver(); - } - - private Set<String> getRecordedProgramsDirs() { - try (Cursor c = mContentResolver.query( - TvContract.RecordedPrograms.CONTENT_URI, mProjection, null, null, null)) { - if (c == null) { - return null; - } - Set<String> recordedProgramDirs = new HashSet<>(); - while (c.moveToNext()) { - String packageName = c.getString(0); - String dataUriString = c.getString(1); - if (dataUriString == null) { - continue; - } - Uri dataUri = Uri.parse(dataUriString); - if (!Utils.isInBundledPackageSet(packageName) - || dataUri == null || dataUri.getPath() == null - || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { - continue; - } - File recordedProgramDir = new File(dataUri.getPath()); - try { - recordedProgramDirs.add(recordedProgramDir.getCanonicalPath()); - } catch (IOException | SecurityException e) { - } - } - return recordedProgramDirs; - } - } - - @Override - protected JobParameters[] doInBackground(JobParameters... params) { - if (mDvrStorageStatusManager.getDvrStorageStatus() - == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { - return params; - } - File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory(); - if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) { - return params; - } - Set<String> recordedProgramDirs = getRecordedProgramsDirs(); - if (recordedProgramDirs == null) { - return params; - } - File[] files = dvrRecordingDir.listFiles(); - if (files == null || files.length == 0) { - return params; - } - for (File recordingDir : files) { - try { - if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) { - long lastModified = recordingDir.lastModified(); - long now = System.currentTimeMillis(); - 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. - Utils.deleteDirOrFile(recordingDir); - } - } - } catch (IOException | SecurityException e) { - // would not happen - } - } - return params; - } - - @Override - protected void onPostExecute(JobParameters[] params) { - for (JobParameters param : params) { - mJobService.jobFinished(param, false); - } - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java deleted file mode 100644 index 2725ddfc..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.tvinput; - -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputService; -import android.util.Log; - -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; -import com.android.tv.TvApplication; -import com.android.tv.common.feature.CommonFeatures; - -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.TimeUnit; - -/** - * {@link TunerTvInputService} serves TV channels coming from a tuner device. - */ -public class TunerTvInputService extends TvInputService - implements AudioCapabilitiesReceiver.Listener{ - private static final String TAG = "TunerTvInputService"; - 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 ChannelDataManager mChannelDataManager; - private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; - private AudioCapabilities mAudioCapabilities; - - @Override - public void onCreate() { - if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) { - Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); - this.stopSelf(); - return; - } - TvApplication.setCurrentRunningProcess(this, false); - 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); - JobInfo pendingJob = jobScheduler.getPendingJob(DVR_STORAGE_CLEANUP_JOB_ID); - if (pendingJob != null) { - // storage cleaning job is already scheduled. - } else { - JobInfo job = new JobInfo.Builder(DVR_STORAGE_CLEANUP_JOB_ID, - new ComponentName(this, TunerStorageCleanUpService.class)) - .setPersisted(true).setPeriodic(TimeUnit.DAYS.toMillis(1)).build(); - jobScheduler.schedule(job); - } - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); - mChannelDataManager.release(); - mAudioCapabilitiesReceiver.unregister(); - } - - @Override - public RecordingSession onCreateRecordingSession(String inputId) { - return new TunerRecordingSession(this, inputId, mChannelDataManager); - } - - @Override - public Session onCreateSession(String inputId) { - if (DEBUG) Log.d(TAG, "onCreateSession"); - try { - final TunerSession session = new TunerSession(this, mChannelDataManager); - mTunerSessions.add(session); - session.setAudioCapabilities(mAudioCapabilities); - session.setOverlayViewEnabled(true); - return session; - } catch (RuntimeException e) { - // There are no available DVB devices. - Log.e(TAG, "Creating a session for " + inputId + " failed.", e); - return null; - } - } - - @Override - public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - mAudioCapabilities = audioCapabilities; - for (TunerSession session : mTunerSessions) { - if (!session.isReleased()) { - session.setAudioCapabilities(audioCapabilities); - } - } - } - - public static String getInputId(Context context) { - return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class)); - } -} diff --git a/src/com/android/tv/tuner/util/ByteArrayBuffer.java b/src/com/android/tv/tuner/util/ByteArrayBuffer.java deleted file mode 100644 index da887e7d..00000000 --- a/src/com/android/tv/tuner/util/ByteArrayBuffer.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/util/ByteArrayBuffer.java $ - * $Revision: 496070 $ - * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.tv.tuner.util; - -/** - * An expandable byte buffer built on byte array. - */ -public final class ByteArrayBuffer { - - private byte[] buffer; - private int len; - - public ByteArrayBuffer(int capacity) { - super(); - if (capacity < 0) { - throw new IllegalArgumentException("Buffer capacity may not be negative"); - } - this.buffer = new byte[capacity]; - } - - private void expand(int newlen) { - byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)]; - System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); - this.buffer = newbuffer; - } - - public void append(final byte[] b, int off, int len) { - if (b == null) { - return; - } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException(); - } - if (len == 0) { - return; - } - int newlen = this.len + len; - if (newlen > this.buffer.length) { - expand(newlen); - } - System.arraycopy(b, off, this.buffer, this.len, len); - this.len = newlen; - } - - public void append(int b) { - int newlen = this.len + 1; - if (newlen > this.buffer.length) { - expand(newlen); - } - this.buffer[this.len] = (byte) b; - this.len = newlen; - } - - public void append(final char[] b, int off, int len) { - if (b == null) { - return; - } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException(); - } - if (len == 0) { - return; - } - int oldlen = this.len; - int newlen = oldlen + len; - if (newlen > this.buffer.length) { - expand(newlen); - } - for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { - this.buffer[i2] = (byte) b[i1]; - } - this.len = newlen; - } - - public void clear() { - this.len = 0; - } - - public byte[] toByteArray() { - byte[] b = new byte[this.len]; - if (this.len > 0) { - System.arraycopy(this.buffer, 0, b, 0, this.len); - } - return b; - } - - public int byteAt(int i) { - return this.buffer[i]; - } - - public int capacity() { - return this.buffer.length; - } - - public int length() { - return this.len; - } - - public byte[] buffer() { - return this.buffer; - } - - public void setLength(int len) { - if (len < 0 || len > this.buffer.length) { - throw new IndexOutOfBoundsException(); - } - this.len = len; - } - - public boolean isEmpty() { - return this.len == 0; - } - - public boolean isFull() { - return this.len == this.buffer.length; - } - -} diff --git a/src/com/android/tv/tuner/util/ConvertUtils.java b/src/com/android/tv/tuner/util/ConvertUtils.java deleted file mode 100644 index abf18d8c..00000000 --- a/src/com/android/tv/tuner/util/ConvertUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -/** - * Utility class for converting date and time. - */ -public class ConvertUtils { - // Time diff between 1.1.1970 00:00:00 and 6.1.1980 00:00:00 - private static final long DIFF_BETWEEN_UNIX_EPOCH_AND_GPS = 315964800; - - private ConvertUtils() { } - - public static long convertGPSTimeToUnixEpoch(long gpsTime) { - return gpsTime + DIFF_BETWEEN_UNIX_EPOCH_AND_GPS; - } - - public static long convertUnixEpochToGPSTime(long epochTime) { - return epochTime - DIFF_BETWEEN_UNIX_EPOCH_AND_GPS; - } -} diff --git a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java deleted file mode 100644 index 0cefcbed..00000000 --- a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java +++ /dev/null @@ -1,36 +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.content.Context; -import android.provider.Settings; - -/** - * Utility class that get information of global settings. - */ -public class GlobalSettingsUtils { - // Since global surround setting is hided, add the related variable here for checking surround - // sound setting when the audio is unavailable. Remove this workaround after b/31254857 fixed. - private static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output"; - public static final int ENCODED_SURROUND_OUTPUT_NEVER = 1; - - private GlobalSettingsUtils () { } - - public static int getEncodedSurroundOutputSettings(Context context) { - return Settings.Global.getInt(context.getContentResolver(), ENCODED_SURROUND_OUTPUT, 0); - } -} diff --git a/src/com/android/tv/tuner/util/Ints.java b/src/com/android/tv/tuner/util/Ints.java deleted file mode 100644 index 0b1be426..00000000 --- a/src/com/android/tv/tuner/util/Ints.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.android.tv.tuner.util; - -import java.util.ArrayList; -import java.util.List; - -/** - * Static utility methods pertaining to int primitives. (Referred Guava's Ints class) - */ -public class Ints { - private Ints() {} - - public static int[] toArray(List<Integer> integerList) { - int[] intArray = new int[integerList.size()]; - int i = 0; - for (Integer data : integerList) { - intArray[i++] = data; - } - return intArray; - } - - public static List<Integer> asList(int[] intArray) { - List<Integer> integerList = new ArrayList<>(intArray.length); - for (int data : intArray) { - integerList.add(data); - } - return integerList; - } -} diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java deleted file mode 100644 index 9eb689a7..00000000 --- a/src/com/android/tv/tuner/util/PostalCodeUtils.java +++ /dev/null @@ -1,138 +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.util; - -import android.content.Context; -import android.location.Address; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.util.LocationUtils; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Pattern; - -/** - * A utility class to update, get, and set the last known postal or zip code. - */ -public class PostalCodeUtils { - private static final String TAG = "PostalCodeUtils"; - - // Postcode formats, where A signifies a letter and 9 a digit: - // US zip code format: 99999 - private static final String POSTCODE_REGEX_US = "^(\\d{5})"; - // UK postcode district formats: A9, A99, AA9, AA99 - // Full UK postcode format: Postcode District + space + 9AA - // Should be able to handle both postcode district and full postcode - private static final String POSTCODE_REGEX_GB = - "^([A-Z][A-Z]?[0-9][0-9A-Z]?)( ?[0-9][A-Z]{2})?$"; - private static final String POSTCODE_REGEX_GB_GIR = "^GIR( ?0AA)?$"; // special UK postcode - - private static final Map<String, Pattern> REGION_PATTERN = new HashMap<>(); - private static final Map<String, Integer> REGION_MAX_LENGTH = new HashMap<>(); - - static { - REGION_PATTERN.put(Locale.US.getCountry(), Pattern.compile(POSTCODE_REGEX_US)); - REGION_PATTERN.put( - Locale.UK.getCountry(), - Pattern.compile(POSTCODE_REGEX_GB + "|" + POSTCODE_REGEX_GB_GIR)); - REGION_MAX_LENGTH.put(Locale.US.getCountry(), 5); - REGION_MAX_LENGTH.put(Locale.UK.getCountry(), 8); - } - - // The longest postcode number is 10-character-long. - // Use a larger number to accommodate future changes. - private static final int DEFAULT_MAX_LENGTH = 16; - - /** Returns {@code true} if postal code has been changed */ - public static boolean updatePostalCode(Context context) - throws IOException, SecurityException, NoPostalCodeException { - String postalCode = getPostalCode(context); - String lastPostalCode = getLastPostalCode(context); - if (TextUtils.isEmpty(postalCode)) { - if (TextUtils.isEmpty(lastPostalCode)) { - throw new NoPostalCodeException(); - } - } else if (!TextUtils.equals(postalCode, lastPostalCode)) { - setLastPostalCode(context, postalCode); - return true; - } - return false; - } - - /** - * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or - * input by users. - */ - public static String getLastPostalCode(Context context) { - return TunerPreferences.getLastPostalCode(context); - } - - /** - * Sets the last stored postal or zip code. This method will overwrite the value written by - * calling {@link #updatePostalCode(Context)}. - */ - public static void setLastPostalCode(Context context, String postalCode) { - Log.i(TAG, "Set Postal Code:" + postalCode); - TunerPreferences.setLastPostalCode(context, postalCode); - } - - @Nullable - private static String getPostalCode(Context context) throws IOException, SecurityException { - Address address = LocationUtils.getCurrentAddress(context); - if (address != null) { - Log.i(TAG, "Current country and postal code is " + address.getCountryName() + ", " - + address.getPostalCode()); - return address.getPostalCode(); - } - return null; - } - - /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */ - public static class NoPostalCodeException extends Exception { - public NoPostalCodeException() { - } - } - - /** - * Checks whether a postcode matches the format of the specific region. - * - * @return {@code false} if the region is supported and the postcode doesn't match; {@code true} - * otherwise - */ - public static boolean matches(@NonNull CharSequence postcode, @NonNull String region) { - Pattern pattern = REGION_PATTERN.get(region.toUpperCase()); - return pattern == null || pattern.matcher(postcode).matches(); - } - - /** - * Gets the largest possible postcode length in the region. - * - * @return maximum postcode length if the region is supported; {@link #DEFAULT_MAX_LENGTH} - * otherwise - */ - public static int getRegionMaxLength(Context context) { - Integer maxLength = - REGION_MAX_LENGTH.get(LocationUtils.getCurrentCountry(context).toUpperCase()); - return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/util/StatusTextUtils.java b/src/com/android/tv/tuner/util/StatusTextUtils.java deleted file mode 100644 index 2633834b..00000000 --- a/src/com/android/tv/tuner/util/StatusTextUtils.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -import java.util.Locale; - -/** - * Utility class for tuner status messages. - */ -public class StatusTextUtils { - private static final int PACKETS_PER_SEC_YELLOW = 1500; - private static final int PACKETS_PER_SEC_RED = 1000; - private static final int AUDIO_POSITION_MS_RATE_DIFF_YELLOW = 100; - private static final int AUDIO_POSITION_MS_RATE_DIFF_RED = 200; - private static final String COLOR_RED = "red"; - private static final String COLOR_YELLOW = "yellow"; - private static final String COLOR_GREEN = "green"; - private static final String COLOR_GRAY = "gray"; - - private StatusTextUtils() { } - - /** - * Returns tuner status warning message in HTML. - * - * <p>This is only called for debuging and always shown in english.</p> - */ - public static String getStatusWarningInHTML(long packetsPerSec, - int videoFrameDrop, int bytesInQueue, - long audioPositionUs, long audioPositionUsRate, - long audioPtsUs, long audioPtsUsRate, - long videoPtsUs, long videoPtsUsRate) { - StringBuffer buffer = new StringBuffer(); - - // audioPosition should go in rate of 1000ms. - long audioPositionMsRate = audioPositionUsRate / 1000; - String audioPositionColor; - if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_RED) { - audioPositionColor = COLOR_RED; - } else if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_YELLOW) { - audioPositionColor = COLOR_YELLOW; - } else { - audioPositionColor = COLOR_GRAY; - } - buffer.append(String.format(Locale.US, "<font color=%s>", audioPositionColor)); - buffer.append( - String.format(Locale.US, "audioPositionMs: %d (%d)<br>", audioPositionUs / 1000, - audioPositionMsRate)); - buffer.append("</font>\n"); - buffer.append("<font color=" + COLOR_GRAY + ">"); - buffer.append(String.format(Locale.US, "audioPtsMs: %d (%d, %d)<br>", audioPtsUs / 1000, - audioPtsUsRate / 1000, (audioPtsUs - audioPositionUs) / 1000)); - buffer.append(String.format(Locale.US, "videoPtsMs: %d (%d, %d)<br>", videoPtsUs / 1000, - videoPtsUsRate / 1000, (videoPtsUs - audioPositionUs) / 1000)); - buffer.append("</font>\n"); - - appendStatusLine(buffer, "KbytesInQueue", bytesInQueue / 1000, 1, 10); - buffer.append("<br/>"); - appendErrorStatusLine(buffer, "videoFrameDrop", videoFrameDrop, 0, 2); - buffer.append("<br/>"); - appendStatusLine(buffer, "packetsPerSec", packetsPerSec, PACKETS_PER_SEC_RED, - PACKETS_PER_SEC_YELLOW); - return buffer.toString(); - } - - /** - * Returns audio unavailable warning message in HTML. - */ - public static String getAudioWarningInHTML(String msg) { - return String.format("<font color=%s>%s</font>\n", COLOR_YELLOW, msg); - } - - private static void appendStatusLine(StringBuffer buffer, String factorName, long value, - int minRed, int minYellow) { - buffer.append("<font color="); - if (value <= minRed) { - buffer.append(COLOR_RED); - } else if (value <= minYellow) { - buffer.append(COLOR_YELLOW); - } else { - buffer.append(COLOR_GREEN); - } - buffer.append(">"); - buffer.append(factorName); - buffer.append(" : "); - buffer.append(value); - buffer.append("</font>"); - } - - private static void appendErrorStatusLine(StringBuffer buffer, String factorName, int value, - int minGreen, int minYellow) { - buffer.append("<font color="); - if (value <= minGreen) { - buffer.append(COLOR_GREEN); - } else if (value <= minYellow) { - buffer.append(COLOR_YELLOW); - } else { - buffer.append(COLOR_RED); - } - buffer.append(">"); - buffer.append(factorName); - buffer.append(" : "); - buffer.append(value); - buffer.append("</font>"); - } -} diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java deleted file mode 100644 index 2817ccbf..00000000 --- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -import android.util.Log; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Proxy class that gives an access to a hidden API {@link android.os.SystemProperties#getBoolean}. - */ -public class SystemPropertiesProxy { - private static final String TAG = "SystemPropertiesProxy"; - - private SystemPropertiesProxy() { } - - public static boolean getBoolean(String key, boolean def) - throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getBooleanMethod = SystemPropertiesClass.getDeclaredMethod("getBoolean", - String.class, boolean.class); - getBooleanMethod.setAccessible(true); - return (boolean) getBooleanMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.getBoolean()", e); - } - return def; - } - - public static int getInt(String key, int def) - throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getIntMethod = SystemPropertiesClass.getDeclaredMethod("getInt", - String.class, int.class); - getIntMethod.setAccessible(true); - return (int) getIntMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.getInt()", e); - } - return def; - } - - public static String getString(String key, String def) throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getIntMethod = - SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class); - getIntMethod.setAccessible(true); - return (String) getIntMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException - | IllegalAccessException - | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.get()", e); - } - return def; - } -} diff --git a/src/com/android/tv/tuner/util/TisConfiguration.java b/src/com/android/tv/tuner/util/TisConfiguration.java deleted file mode 100644 index ca861d67..00000000 --- a/src/com/android/tv/tuner/util/TisConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.tv.tuner.util; - -import android.content.Context; - -/** - * A helper class of tuner configuration. - */ -public class TisConfiguration { - private static final String LC_PACKAGE_NAME = "com.android.tv"; - - public static boolean isPackagedWithLiveChannels(Context context) { - return (LC_PACKAGE_NAME.equals(context.getPackageName())); - } - - public static boolean isInternalTunerTvInput(Context context) { - return (!LC_PACKAGE_NAME.equals(context.getPackageName())); - } - - public static int getTunerHwDeviceId(Context context) { - return 0; // FIXME: Make it OEM configurable - } -} diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java deleted file mode 100644 index f421bf1a..00000000 --- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java +++ /dev/null @@ -1,115 +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.feature.CommonFeatures; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.tvinput.TunerTvInputService; - -/** - * 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 { - TvInputInfo.Builder builder = new TvInputInfo.Builder(context, - new ComponentName(context, TunerTvInputService.class)); - return builder.setLabel(inputLabelId) - .setCanRecord(CommonFeatures.DVR.isEnabled(context)) - .setTunerCount(tunerTypeAndCount.second) - .build(); - } catch (IllegalArgumentException | NullPointerException e) { - // TunerTvInputService 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 (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(); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index 625014ea..b2be9f02 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -21,18 +21,15 @@ import android.media.tv.TvView; import android.util.AttributeSet; import android.view.SurfaceView; import android.view.View; - -import com.android.tv.util.Debug; -import com.android.tv.util.Utils; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.Debug; /** * A TvView class for application layer when multiple windows are being used in the app. - * <p> - * Once an app starts using additional window like SubPanel and it gets window focus, the - * {@link android.media.tv.TvView#setMain()} does not work because its implementation assumes that - * the app uses only application layer. - * TODO: remove this class once the TvView.setMain() is revisited. - * </p> + * + * <p>Once an app starts using additional window like SubPanel and it gets window focus, the {@link + * android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app + * uses only application layer. TODO: remove this class once the TvView.setMain() is revisited. */ public class AppLayerTvView extends TvView { public AppLayerTvView(Context context) { @@ -56,7 +53,7 @@ public class AppLayerTvView extends TvView { public void onViewAdded(View child) { if (child instanceof SurfaceView) { // Note: See b/29118070 for detail. - ((SurfaceView) child).setSecure(!Utils.isDeveloper()); + ((SurfaceView) child).setSecure(!CommonUtils.isDeveloper()); } super.onViewAdded(child); } @@ -66,7 +63,7 @@ public class AppLayerTvView extends TvView { super.getLocationOnScreen(outLocation); // The TvView.MySessionCallback.onSessionCreated() will call this method indirectly. - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "AppLayerTvView.getLocationOnScreen, session created"); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log("AppLayerTvView.getLocationOnScreen, session created"); } } diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java index 09c167ca..6b2d9a01 100644 --- a/src/com/android/tv/ui/BlockScreenView.java +++ b/src/com/android/tv/ui/BlockScreenView.java @@ -29,7 +29,6 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.TextView; - import com.android.tv.R; import com.android.tv.ui.TunableTvView.BlockScreenType; @@ -62,10 +61,11 @@ public class BlockScreenView extends FrameLayout { public BlockScreenView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mSpacingNormal = getResources().getDimensionPixelOffset( - R.dimen.tvview_block_vertical_spacing); - mSpacingShrunken = getResources().getDimensionPixelOffset( - R.dimen.shrunken_tvview_block_vertical_spacing); + mSpacingNormal = + getResources().getDimensionPixelOffset(R.dimen.tvview_block_vertical_spacing); + mSpacingShrunken = + getResources() + .getDimensionPixelOffset(R.dimen.shrunken_tvview_block_vertical_spacing); } @Override @@ -78,66 +78,60 @@ public class BlockScreenView extends FrameLayout { mSpace = findViewById(R.id.space); mBlockingInfoTextView = (TextView) findViewById(R.id.block_screen_text); mBackgroundImageView = (ImageView) findViewById(R.id.background_image); - mFadeOut = AnimatorInflater.loadAnimator(getContext(), - R.animator.tvview_block_screen_fade_out); + mFadeOut = + AnimatorInflater.loadAnimator( + getContext(), R.animator.tvview_block_screen_fade_out); mFadeOut.setTarget(this); - mFadeOut.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - setVisibility(GONE); - setBackgroundImage(null); - setAlpha(1.0f); - } - }); - mInfoFadeIn = AnimatorInflater.loadAnimator(getContext(), - R.animator.tvview_block_screen_fade_in); + mFadeOut.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(GONE); + setBackgroundImage(null); + setAlpha(1.0f); + } + }); + mInfoFadeIn = + AnimatorInflater.loadAnimator(getContext(), R.animator.tvview_block_screen_fade_in); mInfoFadeIn.setTarget(mContainerView); - mInfoFadeOut = AnimatorInflater.loadAnimator(getContext(), - R.animator.tvview_block_screen_fade_out); + mInfoFadeOut = + AnimatorInflater.loadAnimator( + getContext(), R.animator.tvview_block_screen_fade_out); mInfoFadeOut.setTarget(mContainerView); - mInfoFadeOut.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mContainerView.setVisibility(GONE); - } - }); + mInfoFadeOut.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mContainerView.setVisibility(GONE); + } + }); } - /** - * Sets the normal image. - */ + /** Sets the normal image. */ public void setIconImage(int resId) { mNormalLockIconView.setImageResource(resId); updateSpaceVisibility(); } - /** - * Sets the scale type of the normal image. - */ + /** Sets the scale type of the normal image. */ public void setIconScaleType(ScaleType scaleType) { mNormalLockIconView.setScaleType(scaleType); updateSpaceVisibility(); } - /** - * Show or hide the image of this view. - */ + /** Show or hide the image of this view. */ public void setIconVisibility(boolean visible) { mImageContainer.setVisibility(visible ? VISIBLE : GONE); updateSpaceVisibility(); } - /** - * Sets the text message. - */ + /** Sets the text message. */ public void setInfoText(int resId) { mBlockingInfoTextView.setText(resId); updateSpaceVisibility(); } - /** - * Sets the text message. - */ + /** Sets the text message. */ public void setInfoText(String text) { mBlockingInfoTextView.setText(text); updateSpaceVisibility(); @@ -175,19 +169,18 @@ public class BlockScreenView extends FrameLayout { } /** - * Changes the spacing between the image view and the text view according to the - * {@code blockScreenType}. + * Changes the spacing between the image view and the text view according to the {@code + * blockScreenType}. */ public void setSpacing(@BlockScreenType int blockScreenType) { mSpace.getLayoutParams().height = blockScreenType == TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW - ? mSpacingShrunken : mSpacingNormal; + ? mSpacingShrunken + : mSpacingNormal; requestLayout(); } - /** - * Changes the view layout according to the {@code blockScreenType}. - */ + /** Changes the view layout according to the {@code blockScreenType}. */ public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) { if (!withAnimation) { switch (blockScreenType) { @@ -235,25 +228,19 @@ public class BlockScreenView extends FrameLayout { updateSpaceVisibility(); } - /** - * Adds a listener to the fade-in animation of info text and icons of the block screen. - */ + /** Adds a listener to the fade-in animation of info text and icons of the block screen. */ public void addInfoFadeInAnimationListener(AnimatorListener listener) { mInfoFadeIn.addListener(listener); } - /** - * Fades out the block screen. - */ + /** Fades out the block screen. */ public void fadeOut() { if (getVisibility() == VISIBLE && !mFadeOut.isStarted()) { mFadeOut.start(); } } - /** - * Ends the currently running animations. - */ + /** Ends the currently running animations. */ public void endAnimations() { if (mFadeOut != null && mFadeOut.isRunning()) { mFadeOut.end(); diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index a5d897f2..28325197 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -26,7 +26,6 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; -import android.os.Handler; import android.support.annotation.Nullable; import android.text.Spannable; import android.text.SpannableString; @@ -38,6 +37,8 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -45,46 +46,43 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.parental.ContentRatingsManager; -import com.android.tv.util.ImageCache; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.ImageLoader.ImageLoaderCallback; -import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; +import com.android.tv.ui.TvTransitionManager.TransitionLayout; +import com.android.tv.ui.hideable.AutoHideScheduler; import com.android.tv.util.Utils; - -/** - * A view to render channel banner. - */ -public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout { +import com.android.tv.util.images.ImageCache; +import com.android.tv.util.images.ImageLoader; +import com.android.tv.util.images.ImageLoader.ImageLoaderCallback; +import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask; + +/** A view to render channel banner. */ +public class ChannelBannerView extends FrameLayout + implements TransitionLayout, AccessibilityStateChangeListener { private static final String TAG = "ChannelBannerView"; private static final boolean DEBUG = false; - /** - * Show all information at the channel banner. - */ + /** Show all information at the channel banner. */ public static final int LOCK_NONE = 0; /** - * Lock program details at the channel banner. - * This is used when a content is locked so we don't want to show program details - * including program description text and poster art. + * Lock program details at the channel banner. This is used when a content is locked so we don't + * want to show program details including program description text and poster art. */ public static final int LOCK_PROGRAM_DETAIL = 1; /** - * Lock channel information at the channel banner. - * This is used when a channel is locked so we only want to show input information. + * Lock channel information at the channel banner. This is used when a channel is locked so we + * only want to show input information. */ public static final int LOCK_CHANNEL_INFO = 2; @@ -119,7 +117,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private Channel mCurrentChannel; private boolean mCurrentChannelLogoExists; private Program mLastUpdatedProgram; - private final Handler mHandler = new Handler(); + private final AutoHideScheduler mAutoHideScheduler; private final DvrManager mDvrManager; private ContentRatingsManager mContentRatingsManager; private TvContentRating mBlockingContentRating; @@ -134,18 +132,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private final Animator mProgramDescriptionFadeInAnimator; private final Animator mProgramDescriptionFadeOutAnimator; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - mCurrentHeight = 0; - mMainActivity.getOverlayManager().hideOverlays( - TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); - } - }; private final long mShowDurationMillis; private final int mChannelLogoImageViewWidth; private final int mChannelLogoImageViewHeight; @@ -157,22 +143,23 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private final int mRecordingIconPadding; private final Interpolator mResizeInterpolator; - private final AnimatorListenerAdapter mResizeAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - mProgramInfoUpdatePendingByResizing = false; - } + private final AnimatorListenerAdapter mResizeAnimatorListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + mProgramInfoUpdatePendingByResizing = false; + } - @Override - public void onAnimationEnd(Animator animator) { - mProgramDescriptionTextView.setAlpha(1f); - mResizeAnimator = null; - if (mProgramInfoUpdatePendingByResizing) { - mProgramInfoUpdatePendingByResizing = false; - updateProgramInfo(mLastUpdatedProgram); - } - } - }; + @Override + public void onAnimationEnd(Animator animator) { + mProgramDescriptionTextView.setAlpha(1f); + mResizeAnimator = null; + if (mProgramInfoUpdatePendingByResizing) { + mProgramInfoUpdatePendingByResizing = false; + updateProgramInfo(mLastUpdatedProgram); + } + } + }; public ChannelBannerView(Context context) { this(context, null); @@ -185,53 +172,60 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mResources = getResources(); - mMainActivity = (MainActivity) context; - mShowDurationMillis = mResources.getInteger( - R.integer.channel_banner_show_duration); - mChannelLogoImageViewWidth = mResources.getDimensionPixelSize( - R.dimen.channel_banner_channel_logo_width); - mChannelLogoImageViewHeight = mResources.getDimensionPixelSize( - R.dimen.channel_banner_channel_logo_height); - mChannelLogoImageViewMarginStart = mResources.getDimensionPixelSize( - R.dimen.channel_banner_channel_logo_margin_start); - mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize( - R.dimen.channel_banner_program_description_width); + mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration); + mChannelLogoImageViewWidth = + mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_width); + mChannelLogoImageViewHeight = + mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_height); + mChannelLogoImageViewMarginStart = + mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_margin_start); + mProgramDescriptionTextViewWidth = + mResources.getDimensionPixelSize(R.dimen.channel_banner_program_description_width); mChannelBannerTextColor = mResources.getColor(R.color.channel_banner_text_color, null); - mChannelBannerDimTextColor = mResources.getColor(R.color.channel_banner_dim_text_color, - null); + mChannelBannerDimTextColor = + mResources.getColor(R.color.channel_banner_dim_text_color, null); mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration); - mRecordingIconPadding = mResources.getDimensionPixelOffset( - R.dimen.channel_banner_recording_icon_padding); + mRecordingIconPadding = + mResources.getDimensionPixelOffset(R.dimen.channel_banner_recording_icon_padding); - mResizeInterpolator = AnimationUtils.loadInterpolator(context, - android.R.interpolator.linear_out_slow_in); + mResizeInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); - mProgramDescriptionFadeInAnimator = AnimatorInflater.loadAnimator(mMainActivity, - R.animator.channel_banner_program_description_fade_in); - mProgramDescriptionFadeOutAnimator = AnimatorInflater.loadAnimator(mMainActivity, - R.animator.channel_banner_program_description_fade_out); + mProgramDescriptionFadeInAnimator = + AnimatorInflater.loadAnimator( + mMainActivity, R.animator.channel_banner_program_description_fade_in); + mProgramDescriptionFadeOutAnimator = + AnimatorInflater.loadAnimator( + mMainActivity, R.animator.channel_banner_program_description_fade_out); if (CommonFeatures.DVR.isEnabled(mMainActivity)) { - mDvrManager = TvApplication.getSingletons(mMainActivity).getDvrManager(); + mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager(); } else { mDvrManager = null; } - mContentRatingsManager = TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getContentRatingsManager(); - - mNoProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_no_title)) - .setDescription(EMPTY_STRING) - .build(); - mLockedChannelProgram = new Program.Builder() - .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) - .setDescription(EMPTY_STRING) - .build(); + mContentRatingsManager = + TvSingletons.getSingletons(getContext()) + .getTvInputManagerHelper() + .getContentRatingsManager(); + + mNoProgram = + new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_no_title)) + .setDescription(EMPTY_STRING) + .build(); + mLockedChannelProgram = + new Program.Builder() + .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) + .setDescription(EMPTY_STRING) + .build(); if (sClosedCaptionMark == null) { sClosedCaptionMark = context.getString(R.string.closed_caption); } + mAutoHideScheduler = new AutoHideScheduler(context, this::hide); + context.getSystemService(AccessibilityManager.class) + .addAccessibilityStateChangeListener(mAutoHideScheduler); } @Override @@ -260,12 +254,13 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView); mProgramDescriptionFadeOutAnimator.setTarget(mProgramDescriptionTextView); - mProgramDescriptionFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mProgramDescriptionTextView.setText(mProgramDescriptionText); - } - }); + mProgramDescriptionFadeOutAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mProgramDescriptionTextView.setText(mProgramDescriptionText); + } + }); } @Override @@ -274,22 +269,13 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (fromEmptyScene) { ViewUtils.setTransitionAlpha(mChannelView, 1f); } - scheduleHide(); + mAutoHideScheduler.schedule(mShowDurationMillis); } @Override public void onExitAction() { mCurrentHeight = 0; - cancelHide(); - } - - private void scheduleHide() { - cancelHide(); - mHandler.postDelayed(mHideRunnable, mShowDurationMillis); - } - - private void cancelHide() { - mHandler.removeCallbacks(mHideRunnable); + mAutoHideScheduler.cancel(); } private void resetAnimationEffects() { @@ -308,7 +294,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage * @throws IllegalArgumentException if lockType is invalid. */ public int setLockType(int lockType) { - if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO + if (lockType != LOCK_NONE + && lockType != LOCK_CHANNEL_INFO && lockType != LOCK_PROGRAM_DETAIL) { throw new IllegalArgumentException("No such lock type " + lockType); } @@ -330,7 +317,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage * Update channel banner view. * * @param updateOnTune {@false} denotes the channel banner is updated due to other reasons than - * tuning. The channel info will not be updated in this case. + * tuning. The channel info will not be updated in this case. */ public void updateViews(boolean updateOnTune) { resetAnimationEffects(); @@ -338,7 +325,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mUpdateOnTune = updateOnTune; if (mUpdateOnTune) { if (isShown()) { - scheduleHide(); + mAutoHideScheduler.schedule(mShowDurationMillis); } mBlockingContentRating = null; mCurrentChannel = mMainActivity.getCurrentChannel(); @@ -351,6 +338,18 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mUpdateOnTune = false; } + private void hide() { + mCurrentHeight = 0; + mMainActivity + .getOverlayManager() + .hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + } + /** * Update channel banner view with stream info. * @@ -359,14 +358,18 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage public void updateStreamInfo(StreamInfo info) { // Update stream information in a channel. if (mLockType != LOCK_CHANNEL_INFO && info != null) { - updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark - : EMPTY_STRING); - updateText(mAspectRatioTextView, + updateText( + mClosedCaptionTextView, + info.hasClosedCaption() ? sClosedCaptionMark : EMPTY_STRING); + updateText( + mAspectRatioTextView, Utils.getAspectRatioString(info.getVideoDisplayAspectRatio())); - updateText(mResolutionTextView, + updateText( + mResolutionTextView, Utils.getVideoDefinitionLevelString( mMainActivity, info.getVideoDefinitionLevel())); - updateText(mAudioChannelTextView, + updateText( + mAudioChannelTextView, Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount())); } else { // Channel change has been requested. But, StreamInfo hasn't been updated yet. @@ -415,9 +418,11 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } mChannelNumberTextView.setText(displayNumber); mChannelNameTextView.setText(displayName); - TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo( - getCurrentInputId()); - if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this), + TvInputInfo info = + mMainActivity.getTvInputManagerHelper().getTvInputInfo(getCurrentInputId()); + if (info == null + || !ImageLoader.loadBitmap( + createTvInputLogoLoaderCallback(info, this), new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) { mTvInputLogoImageView.setVisibility(View.GONE); mTvInputLogoImageView.setImageDrawable(null); @@ -425,8 +430,11 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mChannelLogoImageView.setImageBitmap(null); mChannelLogoImageView.setVisibility(View.GONE); if (mCurrentChannel != null && mCurrentChannelLogoExists) { - mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, - mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, + mCurrentChannel.loadBitmap( + getContext(), + Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mChannelLogoImageViewWidth, + mChannelLogoImageViewHeight, createChannelLogoCallback(this, mCurrentChannel)); } } @@ -446,7 +454,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) { @Override public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) { - if (bitmap != null && channelBannerView.mCurrentChannel != null + if (bitmap != null + && channelBannerView.mCurrentChannel != null && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) { channelBannerView.updateTvInputLogo(bitmap); } @@ -485,7 +494,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) { @Override public void onBitmapLoaded(ChannelBannerView view, @Nullable Bitmap logo) { - if (channel != view.mCurrentChannel) { + if (channel.equals(view.mCurrentChannel)) { // The logo is obsolete. return; } @@ -524,8 +533,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (mLastUpdatedProgram == null || !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle()) - || !TextUtils.equals(program.getEpisodeDisplayTitle(getContext()), - mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { + || !TextUtils.equals( + program.getEpisodeDisplayTitle(getContext()), + mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { updateProgramTextView(program); } updateProgramTimeInfo(program); @@ -548,8 +558,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mProgramDescriptionText = program.getDescription(); } String description = mProgramDescriptionTextView.getText().toString(); - boolean programDescriptionNeedFadeAnimation = (isProgramChanged - || !description.equals(mProgramDescriptionText)) && !mUpdateOnTune; + boolean programDescriptionNeedFadeAnimation = + (isProgramChanged || !description.equals(mProgramDescriptionText)) + && !mUpdateOnTune; updateBannerHeight(programDescriptionNeedFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; @@ -561,19 +572,21 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (program == null) { return; } - updateProgramTextView(program == mLockedChannelProgram, program.getTitle(), + updateProgramTextView( + program.equals(mLockedChannelProgram), + program.getTitle(), program.getEpisodeDisplayTitle(getContext())); } - private void updateProgramTextView(boolean dimText, String title, - String episodeDisplayTitle) { + private void updateProgramTextView(boolean dimText, String title, String episodeDisplayTitle) { mProgramTextView.setVisibility(View.VISIBLE); if (dimText) { mProgramTextView.setTextColor(mChannelBannerDimTextColor); } else { mProgramTextView.setTextColor(mChannelBannerTextColor); } - updateTextView(mProgramTextView, + updateTextView( + mProgramTextView, R.dimen.channel_banner_program_large_text_size, R.dimen.channel_banner_program_large_margin_top); if (TextUtils.isEmpty(episodeDisplayTitle)) { @@ -582,18 +595,24 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage String fullTitle = title + " " + episodeDisplayTitle; SpannableString text = new SpannableString(fullTitle); - text.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_channel_banner_episode_title), - fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(), + text.setSpan( + new TextAppearanceSpan( + getContext(), R.style.text_appearance_channel_banner_episode_title), + fullTitle.length() - episodeDisplayTitle.length(), + fullTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mProgramTextView.setText(text); } - int width = mProgramDescriptionTextViewWidth + (mCurrentChannelLogoExists ? - 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); + int width = + mProgramDescriptionTextViewWidth + + (mCurrentChannelLogoExists + ? 0 + : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams(); lp.width = width; mProgramTextView.setLayoutParams(lp); - mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + mProgramTextView.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); boolean oneline = (mProgramTextView.getLineCount() == 1); @@ -602,13 +621,16 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mProgramTextView, R.dimen.channel_banner_program_medium_text_size, R.dimen.channel_banner_program_medium_margin_top); - mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + mProgramTextView.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); oneline = (mProgramTextView.getLineCount() == 1); } - updateTopMargin(mAnchorView, oneline - ? R.dimen.channel_banner_anchor_one_line_y - : R.dimen.channel_banner_anchor_two_line_y); + updateTopMargin( + mAnchorView, + oneline + ? R.dimen.channel_banner_anchor_one_line_y + : R.dimen.channel_banner_anchor_two_line_y); } private void updateProgramRatings(Program program) { @@ -650,8 +672,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) { mProgramTimeTextView.setVisibility(View.VISIBLE); mRemainingTimeView.setVisibility(View.VISIBLE); - mProgramTimeTextView.setText(Utils.getDurationString( - getContext(), startTimeMs, endTimeMs, true)); + mProgramTimeTextView.setText( + Utils.getDurationString(getContext(), startTimeMs, endTimeMs, true)); } else { mProgramTimeTextView.setVisibility(View.GONE); mRemainingTimeView.setVisibility(View.GONE); @@ -673,8 +695,10 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage updateProgressBarAndRecIcon(program, null); return; } - ScheduledRecording currentRecording = (mCurrentChannel == null) ? null - : mDvrManager.getCurrentRecording(mCurrentChannel.getId()); + ScheduledRecording currentRecording = + (mCurrentChannel == null) + ? null + : mDvrManager.getCurrentRecording(mCurrentChannel.getId()); if (DEBUG) { Log.d(TAG, currentRecording == null ? "No Recording" : "Recording:" + currentRecording); } @@ -685,22 +709,23 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } } - private void updateProgressBarAndRecIcon(Program program, - @Nullable ScheduledRecording recording) { + private void updateProgressBarAndRecIcon( + Program program, @Nullable ScheduledRecording recording) { long programStartTime = program.getStartTimeUtcMillis(); long programEndTime = program.getEndTimeUtcMillis(); long currentPosition = mMainActivity.getCurrentPlayingPosition(); updateRecordingIndicator(recording); if (recording != null) { // Recording now. Use recording-style progress bar. - mRemainingTimeView.setProgress(getProgressPercent(recording.getStartTimeMs(), - programStartTime, programEndTime)); - mRemainingTimeView.setSecondaryProgress(getProgressPercent(currentPosition, - programStartTime, programEndTime)); + mRemainingTimeView.setProgress( + getProgressPercent( + recording.getStartTimeMs(), programStartTime, programEndTime)); + mRemainingTimeView.setSecondaryProgress( + getProgressPercent(currentPosition, programStartTime, programEndTime)); } else { // No recording is going now. Recover progress bar. - mRemainingTimeView.setProgress(getProgressPercent(currentPosition, - programStartTime, programEndTime)); + mRemainingTimeView.setProgress( + getProgressPercent(currentPosition, programStartTime, programEndTime)); mRemainingTimeView.setSecondaryProgress(0); } } @@ -708,9 +733,15 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private void updateRecordingIndicator(@Nullable ScheduledRecording recording) { if (recording != null) { if (mRemainingTimeView.getVisibility() == View.GONE) { - mRecordingIndicatorView.setText(mMainActivity.getResources().getString( - R.string.dvr_recording_till_format, DateUtils.formatDateTime(mMainActivity, - recording.getEndTimeMs(), DateUtils.FORMAT_SHOW_TIME))); + mRecordingIndicatorView.setText( + mMainActivity + .getResources() + .getString( + R.string.dvr_recording_till_format, + DateUtils.formatDateTime( + mMainActivity, + recording.getEndTimeMs(), + DateUtils.FORMAT_SHOW_TIME))); mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding); } else { mRecordingIndicatorView.setText(""); @@ -725,10 +756,10 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private boolean isCurrentProgram(ScheduledRecording recording, Program program) { long currentPosition = mMainActivity.getCurrentPlayingPosition(); return (recording.getType() == ScheduledRecording.TYPE_PROGRAM - && recording.getProgramId() == program.getId()) + && recording.getProgramId() == program.getId()) || (recording.getType() == ScheduledRecording.TYPE_TIMED - && currentPosition >= recording.getStartTimeMs() - && currentPosition <= recording.getEndTimeMs()); + && currentPosition >= recording.getStartTimeMs() + && currentPosition <= recording.getEndTimeMs()); } private void setLastUpdatedProgram(Program program) { @@ -764,18 +795,20 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private Animator createResizeAnimator(int targetHeight, boolean addFadeAnimation) { final ValueAnimator heightAnimator = ValueAnimator.ofInt(mCurrentHeight, targetHeight); - heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - LayoutParams layoutParams = (LayoutParams) ChannelBannerView.this.getLayoutParams(); - if (value != layoutParams.height) { - layoutParams.height = value; - ChannelBannerView.this.setLayoutParams(layoutParams); - } - mCurrentHeight = value; - } - }); + heightAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int value = (Integer) animation.getAnimatedValue(); + LayoutParams layoutParams = + (LayoutParams) ChannelBannerView.this.getLayoutParams(); + if (value != layoutParams.height) { + layoutParams.height = value; + ChannelBannerView.this.setLayoutParams(layoutParams); + } + mCurrentHeight = value; + } + }); heightAnimator.setDuration(mResizeAnimDuration); heightAnimator.setInterpolator(mResizeInterpolator); @@ -792,4 +825,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage animator.addListener(mResizeAnimatorListener); return animator; } -}
\ No newline at end of file + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mAutoHideScheduler.onAccessibilityStateChanged(enabled); + } +} diff --git a/src/com/android/tv/ui/DialogUtils.java b/src/com/android/tv/ui/DialogUtils.java index acbaf8c8..341db2e1 100644 --- a/src/com/android/tv/ui/DialogUtils.java +++ b/src/com/android/tv/ui/DialogUtils.java @@ -20,7 +20,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; - import com.android.tv.common.SoftPreconditions; public final class DialogUtils { @@ -31,31 +30,28 @@ public final class DialogUtils { * @param itemResIds String resource id for each item * @param runnables Runnable for each item */ - public static void showListDialog(Context context, int[] itemResIds, - final Runnable[] runnables) { + public static void showListDialog( + Context context, int[] itemResIds, final Runnable[] runnables) { int size = itemResIds.length; SoftPreconditions.checkState(size == runnables.length); - DialogInterface.OnClickListener onClickListener - = new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - Runnable runnable = runnables[which]; - if (runnable != null) { - runnable.run(); - } - dialog.dismiss(); - } - }; + DialogInterface.OnClickListener onClickListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, int which) { + Runnable runnable = runnables[which]; + if (runnable != null) { + runnable.run(); + } + dialog.dismiss(); + } + }; CharSequence[] items = new CharSequence[itemResIds.length]; Resources res = context.getResources(); for (int i = 0; i < size; ++i) { items[i] = res.getString(itemResIds[i]); } - new AlertDialog.Builder(context) - .setItems(items, onClickListener) - .create() - .show(); + new AlertDialog.Builder(context).setItems(items, onClickListener).create().show(); } - private DialogUtils() { } + private DialogUtils() {} } diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java index e2220722..800fa85a 100644 --- a/src/com/android/tv/ui/FullscreenDialogView.java +++ b/src/com/android/tv/ui/FullscreenDialogView.java @@ -26,7 +26,6 @@ import android.view.View; import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; - import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dialog.FullscreenDialogFragment; @@ -58,41 +57,39 @@ public class FullscreenDialogView extends FrameLayout public FullscreenDialogView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mLinearOutSlowIn = AnimationUtils.loadInterpolator(context, - android.R.interpolator.linear_out_slow_in); - mFastOutLinearIn = AnimationUtils.loadInterpolator(context, - android.R.interpolator.fast_out_linear_in); - getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getViewTreeObserver().removeOnGlobalLayoutListener(this); - startEnterAnimation(); - } - }); + mLinearOutSlowIn = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); + mFastOutLinearIn = + AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); + getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + startEnterAnimation(); + } + }); } protected MainActivity getActivity() { return mActivity; } - /** - * Gets the host {@link Dialog}. - */ + /** Gets the host {@link Dialog}. */ protected Dialog getDialog() { return mDialog; } - /** - * Dismisses the host {@link Dialog}. - */ + /** Dismisses the host {@link Dialog}. */ protected void dismiss() { - startExitAnimation(new Runnable() { - @Override - public void run() { - mDialog.dismiss(); - } - }); + startExitAnimation( + new Runnable() { + @Override + public void run() { + mDialog.dismiss(); + } + }); } @Override @@ -102,51 +99,48 @@ public class FullscreenDialogView extends FrameLayout } @Override - public void onBackPressed() { } + public void onBackPressed() {} @Override - public void onDestroy() { } + public void onDestroy() {} - /** - * Transitions to another view inside the host {@link Dialog}. - */ + /** Transitions to another view inside the host {@link Dialog}. */ public void transitionTo(final FullscreenDialogView v) { mSkipExitAlphaAnimation = true; v.mSkipEnterAlphaAnimation = true; v.initialize(mActivity, mDialog); - startExitAnimation(new Runnable() { - @Override - public void run() { - new Handler().postDelayed(new Runnable() { + startExitAnimation( + new Runnable() { @Override public void run() { - v.initialize(getActivity(), getDialog()); - getDialog().setContentView(v); + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + v.initialize(getActivity(), getDialog()); + getDialog().setContentView(v); + } + }, + TRANSITION_INTERVAL_MS); } - }, TRANSITION_INTERVAL_MS); - } - }); + }); } - /** - * Called when an enter animation starts. Sub-view specific animation can be implemented. - */ - protected void onStartEnterAnimation(TimeInterpolator interpolator, long duration) { - } + /** Called when an enter animation starts. Sub-view specific animation can be implemented. */ + protected void onStartEnterAnimation(TimeInterpolator interpolator, long duration) {} - /** - * Called when an exit animation starts. Sub-view specific animation can be implemented. - */ - protected void onStartExitAnimation(TimeInterpolator interpolator, long duration, - Runnable onAnimationEnded) { - } + /** Called when an exit animation starts. Sub-view specific animation can be implemented. */ + protected void onStartExitAnimation( + TimeInterpolator interpolator, long duration, Runnable onAnimationEnded) {} private void startEnterAnimation() { if (DEBUG) Log.d(TAG, "start an enter animation"); View backgroundView = findViewById(R.id.background); if (!mSkipEnterAlphaAnimation) { backgroundView.setAlpha(0); - backgroundView.animate() + backgroundView + .animate() .alpha(1.0f) .setInterpolator(mLinearOutSlowIn) .setDuration(FADE_IN_DURATION_MS) @@ -160,7 +154,8 @@ public class FullscreenDialogView extends FrameLayout if (DEBUG) Log.d(TAG, "start an exit animation"); View backgroundView = findViewById(R.id.background); if (!mSkipExitAlphaAnimation) { - backgroundView.animate() + backgroundView + .animate() .alpha(0.0f) .setInterpolator(mFastOutLinearIn) .setDuration(FADE_OUT_DURATION_MS) diff --git a/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java b/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java index 39ec1279..9685b04b 100644 --- a/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java +++ b/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java @@ -20,17 +20,13 @@ import android.content.Context; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; - import com.android.tv.R; -/** - * Extended stylist class used for {@link GuidedStepFragment} with divider support. - */ +/** Extended stylist class used for {@link GuidedStepFragment} with divider support. */ public class GuidedActionsStylistWithDivider extends GuidedActionsStylist { - /** - * ID used mark a divider. - */ + /** ID used mark a divider. */ public static final int ACTION_DIVIDER = -100; + private static final int VIEW_TYPE_DIVIDER = 1; @Override @@ -50,8 +46,8 @@ public class GuidedActionsStylistWithDivider extends GuidedActionsStylist { } /** - * Creates a divider for {@link GuidedStepFragment}, targeted fragments must use - * {@link GuidedActionsStylistWithDivider} as its actions' stylist for divider to work. + * Creates a divider for {@link GuidedStepFragment}, targeted fragments must use {@link + * GuidedActionsStylistWithDivider} as its actions' stylist for divider to work. */ public static GuidedAction createDividerAction(Context context) { return new GuidedAction.Builder(context) diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java index 4e254c62..5ac715bf 100644 --- a/src/com/android/tv/ui/InputBannerView.java +++ b/src/com/android/tv/ui/InputBannerView.java @@ -23,25 +23,27 @@ import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; public class InputBannerView extends LinearLayout implements TvTransitionManager.TransitionLayout { private final long mShowDurationMillis; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - ((MainActivity) getContext()).getOverlayManager().hideOverlays( - TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); - } - }; + private final Runnable mHideRunnable = + new Runnable() { + @Override + public void run() { + ((MainActivity) getContext()) + .getOverlayManager() + .hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + } + }; private TextView mInputLabelTextView; private TextView mSecondaryInputLabelTextView; @@ -56,8 +58,8 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager public InputBannerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mShowDurationMillis = context.getResources().getInteger( - R.integer.select_input_show_duration); + mShowDurationMillis = + context.getResources().getInteger(R.integer.select_input_show_duration); } @Override @@ -73,8 +75,8 @@ public class InputBannerView extends LinearLayout implements TvTransitionManager if (channel == null || !channel.isPassthrough()) { return; } - TvInputInfo input = mainActivity.getTvInputManagerHelper().getTvInputInfo( - channel.getInputId()); + TvInputInfo input = + mainActivity.getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); CharSequence customLabel = input.loadCustomLabel(getContext()); CharSequence label = input.loadLabel(getContext()); if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) { diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java index 7530f283..be9fb691 100644 --- a/src/com/android/tv/ui/IntroView.java +++ b/src/com/android/tv/ui/IntroView.java @@ -22,7 +22,6 @@ import android.graphics.drawable.AnimationDrawable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; - import com.android.tv.R; import com.android.tv.menu.Menu; @@ -95,20 +94,21 @@ public class IntroView extends FullscreenDialogView { } @Override - protected void onStartExitAnimation(TimeInterpolator interpolator, long duration, - final Runnable onAnimationEnded) { + protected void onStartExitAnimation( + TimeInterpolator interpolator, long duration, final Runnable onAnimationEnded) { View v = findViewById(R.id.container); v.animate() .alpha(0.0f) .setInterpolator(interpolator) .setDuration(duration) .withLayer() - .withEndAction(new Runnable() { - @Override - public void run() { - onAnimationEnded.run(); - } - }) + .withEndAction( + new Runnable() { + @Override + public void run() { + onAnimationEnded.run(); + } + }) .start(); } } diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index ac5d841d..e2625811 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -35,21 +35,19 @@ import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.common.util.DurationTimer; import com.android.tv.data.ChannelNumber; - +import com.android.tv.data.api.Channel; import java.util.ArrayList; import java.util.List; -public class KeypadChannelSwitchView extends LinearLayout implements - TvTransitionManager.TransitionLayout { +public class KeypadChannelSwitchView extends LinearLayout + implements TvTransitionManager.TransitionLayout { private static final String TAG = "KeypadChannelSwitchView"; private static final int MAX_CHANNEL_NUMBER_DIGIT = 4; @@ -62,7 +60,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); private boolean mNavigated = false; - @Nullable //Once mChannels is set to null it should not be used again. + @Nullable // Once mChannels is set to null it should not be used again. private List<Channel> mChannels; private TextView mChannelNumberView; private ListView mChannelItemListView; @@ -72,23 +70,29 @@ public class KeypadChannelSwitchView extends LinearLayout implements private final LayoutInflater mLayoutInflater; private Channel mSelectedChannel; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - mCurrentHeight = 0; - if (mSelectedChannel != null) { - mMainActivity.tuneToChannel(mSelectedChannel); - mTracker.sendChannelNumberItemChosenByTimeout(); - } else { - mMainActivity.getOverlayManager().hideOverlays( - TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); - } - } - }; + private final Runnable mHideRunnable = + new Runnable() { + @Override + public void run() { + mCurrentHeight = 0; + if (mSelectedChannel != null) { + mMainActivity.tuneToChannel(mSelectedChannel); + mTracker.sendChannelNumberItemChosenByTimeout(); + } else { + mMainActivity + .getOverlayManager() + .hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU + | TvOverlayManager + .FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + } + } + }; private final long mShowDurationMillis; private final long mRippleAnimDurationMillis; private final int mBaseViewHeight; @@ -112,65 +116,71 @@ public class KeypadChannelSwitchView extends LinearLayout implements super(context, attrs, defStyleAttr); mMainActivity = (MainActivity) context; - mTracker = TvApplication.getSingletons(context).getTracker(); + mTracker = TvSingletons.getSingletons(context).getTracker(); Resources resources = getResources(); mLayoutInflater = LayoutInflater.from(context); mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration); - mRippleAnimDurationMillis = resources.getInteger( - R.integer.keypad_channel_switch_ripple_anim_duration); - mBaseViewHeight = resources.getDimensionPixelSize( - R.dimen.keypad_channel_switch_base_height); + mRippleAnimDurationMillis = + resources.getInteger(R.integer.keypad_channel_switch_ripple_anim_duration); + mBaseViewHeight = + resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_base_height); mItemHeight = resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_item_height); mResizeAnimDuration = resources.getInteger(R.integer.keypad_channel_switch_anim_duration); - mResizeInterpolator = AnimationUtils.loadInterpolator(context, - android.R.interpolator.linear_out_slow_in); + mResizeInterpolator = + AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); } @Override - protected void onFinishInflate(){ + protected void onFinishInflate() { super.onFinishInflate(); mChannelNumberView = (TextView) findViewById(R.id.channel_number); mChannelItemListView = (ListView) findViewById(R.id.channel_list); mChannelItemListView.setAdapter(mAdapter); - mChannelItemListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - if (position >= mAdapter.getCount()) { - // It can happen during closing. - return; - } - mChannelItemListView.setFocusable(false); - final Channel channel = ((Channel) mAdapter.getItem(position)); - postDelayed(new Runnable() { + mChannelItemListView.setOnItemClickListener( + new AdapterView.OnItemClickListener() { @Override - public void run() { - mChannelItemListView.setFocusable(true); - mMainActivity.tuneToChannel(channel); - mTracker.sendChannelNumberItemClicked(); + public void onItemClick( + AdapterView<?> parent, View view, int position, long id) { + if (position >= mAdapter.getCount()) { + // It can happen during closing. + return; + } + mChannelItemListView.setFocusable(false); + final Channel channel = ((Channel) mAdapter.getItem(position)); + postDelayed( + new Runnable() { + @Override + public void run() { + mChannelItemListView.setFocusable(true); + mMainActivity.tuneToChannel(channel); + mTracker.sendChannelNumberItemClicked(); + } + }, + mRippleAnimDurationMillis); + } + }); + mChannelItemListView.setOnItemSelectedListener( + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected( + AdapterView<?> parent, View view, int position, long id) { + if (position >= mAdapter.getCount()) { + // It can happen during closing. + mSelectedChannel = null; + } else { + mSelectedChannel = (Channel) mAdapter.getItem(position); + } + if (position != 0 && !mNavigated) { + mNavigated = true; + mTracker.sendChannelInputNavigated(); + } } - }, mRippleAnimDurationMillis); - } - }); - mChannelItemListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - if (position >= mAdapter.getCount()) { - // It can happen during closing. - mSelectedChannel = null; - } else { - mSelectedChannel = (Channel) mAdapter.getItem(position); - } - if (position != 0 && !mNavigated) { - mNavigated = true; - mTracker.sendChannelInputNavigated(); - } - } - @Override - public void onNothingSelected(AdapterView<?> parent) { - mSelectedChannel = null; - } - }); + @Override + public void onNothingSelected(AdapterView<?> parent) { + mSelectedChannel = null; + } + }); } @Override @@ -276,8 +286,13 @@ public class KeypadChannelSwitchView extends LinearLayout implements for (Channel channel : mChannels) { ChannelNumber chNumber = ChannelNumber.parseChannelNumber(channel.getDisplayNumber()); if (chNumber == null) { - Log.i(TAG, "Malformed channel number (name=" + channel.getDisplayName() - + ", number=" + channel.getDisplayNumber() + ")"); + Log.i( + TAG, + "Malformed channel number (name=" + + channel.getDisplayName() + + ", number=" + + channel.getDisplayNumber() + + ")"); continue; } if (matchChannelNumber(mTypedChannelNumber, chNumber)) { @@ -286,7 +301,8 @@ public class KeypadChannelSwitchView extends LinearLayout implements // Even if a user doesn't type '-', we need to match the typed number to not only // the major number but also the minor number. For example, when a user types '111' // without delimiter, it should be matched to '111', '1-11' and '11-1'. - if (channel.getDisplayNumber().replaceAll(CHANNEL_DELIMITERS_REGEX, "") + if (channel.getDisplayNumber() + .replaceAll(CHANNEL_DELIMITERS_REGEX, "") .startsWith(mTypedChannelNumber.majorNumber)) { secondaryChannelCandidates.add(channel); } @@ -315,7 +331,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements // Do not add the resize animation when the banner has not been shown before. mCurrentHeight = targetHeight; setViewHeight(this, targetHeight); - } else if (mCurrentHeight != targetHeight){ + } else if (mCurrentHeight != targetHeight) { mResizeAnimator = createResizeAnimator(targetHeight); mResizeAnimator.start(); } @@ -323,21 +339,23 @@ public class KeypadChannelSwitchView extends LinearLayout implements private Animator createResizeAnimator(int targetHeight) { ValueAnimator animator = ValueAnimator.ofInt(mCurrentHeight, targetHeight); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - setViewHeight(KeypadChannelSwitchView.this, value); - mCurrentHeight = value; - } - }); + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int value = (Integer) animation.getAnimatedValue(); + setViewHeight(KeypadChannelSwitchView.this, value); + mCurrentHeight = value; + } + }); animator.setDuration(mResizeAnimDuration); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mResizeAnimator = null; - } - }); + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + mResizeAnimator = null; + } + }); animator.setInterpolator(mResizeInterpolator); return animator; } diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java index 63ee199d..9b916afe 100644 --- a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java +++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java @@ -21,18 +21,15 @@ import android.support.v17.leanback.widget.VerticalGridView; import android.util.Log; import android.view.KeyEvent; import android.view.View; - import com.android.tv.common.WeakHandler; -/** - * Listener to make focus change faster over time. - */ +/** Listener to make focus change faster over time. */ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInterceptListener { private static final String TAG = "OnRepeatedKeyListener"; private static final boolean DEBUG = false; - private static final int[] THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS = { 2000, 5000 }; - private static final int[] MAX_SKIPPED_VIEW_COUNT = { 1, 4 }; + private static final int[] THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS = {2000, 5000}; + private static final int[] MAX_SKIPPED_VIEW_COUNT = {1, 4}; private static final int MSG_MOVE_FOCUS = 1000; private final VerticalGridView mView; @@ -52,21 +49,20 @@ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInt @Override public boolean onInterceptKeyEvent(KeyEvent event) { mHandler.removeMessages(MSG_MOVE_FOCUS); - if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_UP && - event.getKeyCode() != KeyEvent.KEYCODE_DPAD_DOWN) { + if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_UP + && event.getKeyCode() != KeyEvent.KEYCODE_DPAD_DOWN) { return false; } long duration = event.getEventTime() - event.getDownTime(); - if (duration < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[0] - || event.isCanceled()) { + if (duration < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[0] || event.isCanceled()) { mFocusAccelerated = false; return false; } - mDirection = event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP ? View.FOCUS_UP - : View.FOCUS_DOWN; + mDirection = + event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP ? View.FOCUS_UP : View.FOCUS_DOWN; int skippedViewCount = MAX_SKIPPED_VIEW_COUNT[0]; - for (int i = 1 ;i < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS.length; ++i) { + for (int i = 1; i < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS.length; ++i) { if (THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[i] < duration) { skippedViewCount = MAX_SKIPPED_VIEW_COUNT[i]; } else { @@ -83,8 +79,8 @@ public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInt mFocusAccelerated = false; } for (int i = 0; i < skippedViewCount; ++i) { - mHandler.sendEmptyMessageDelayed(MSG_MOVE_FOCUS, - mRepeatedKeyInterval * i / (skippedViewCount + 1)); + mHandler.sendEmptyMessageDelayed( + MSG_MOVE_FOCUS, mRepeatedKeyInterval * i / (skippedViewCount + 1)); } if (DEBUG) Log.d(TAG, "onInterceptKeyEvent: focused view " + mView.findFocus()); return false; diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index dc92111c..f4949f08 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -32,23 +32,20 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; -import com.android.tv.data.Channel; +import com.android.tv.common.util.DurationTimer; +import com.android.tv.data.api.Channel; import com.android.tv.util.TvInputManagerHelper; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -public class SelectInputView extends VerticalGridView implements - TvTransitionManager.TransitionLayout { +public class SelectInputView extends VerticalGridView + implements TvTransitionManager.TransitionLayout { private static final String TAG = "SelectInputView"; private static final boolean DEBUG = false; public static final String SCREEN_NAME = "Input selection"; @@ -59,66 +56,68 @@ public class SelectInputView extends VerticalGridView implements private final TvInputManagerHelper.HardwareInputComparator mComparator; private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); - private final TvInputCallback mTvInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } + private final TvInputCallback mTvInputCallback = + new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } - @Override - public void onInputRemoved(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } + @Override + public void onInputRemoved(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } - @Override - public void onInputUpdated(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } + @Override + public void onInputUpdated(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } - @Override - public void onInputStateChanged(String inputId, int state) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } + @Override + public void onInputStateChanged(String inputId, int state) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } - private void updateSelectedPositionIfNeeded() { - if (!isFocusable() || mSelectedInput == null) { - return; - } - if (!isInputEnabled(mSelectedInput)) { - setSelectedPosition(TUNER_INPUT_POSITION); - return; - } - if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) { - setSelectedPosition(getInputPosition(mSelectedInput.getId())); - } - } - }; + private void updateSelectedPositionIfNeeded() { + if (!isFocusable() || mSelectedInput == null) { + return; + } + if (!isInputEnabled(mSelectedInput)) { + setSelectedPosition(TUNER_INPUT_POSITION); + return; + } + if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) { + setSelectedPosition(getInputPosition(mSelectedInput.getId())); + } + } + }; private Channel mCurrentChannel; private OnInputSelectedCallback mCallback; - private final Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - if (mSelectedInput == null) { - return; - } - // TODO: pass english label to tracker http://b/22355024 - final String label = mSelectedInput.loadLabel(getContext()).toString(); - mTracker.sendInputSelected(label); - if (mCallback != null) { - if (mSelectedInput.isPassthroughInput()) { - mCallback.onPassthroughInputSelected(mSelectedInput); - } else { - mCallback.onTunerInputSelected(); + private final Runnable mHideRunnable = + new Runnable() { + @Override + public void run() { + if (mSelectedInput == null) { + return; + } + // TODO: pass english label to tracker http://b/22355024 + final String label = mSelectedInput.loadLabel(getContext()).toString(); + mTracker.sendInputSelected(label); + if (mCallback != null) { + if (mSelectedInput.isPassthroughInput()) { + mCallback.onPassthroughInputSelected(mSelectedInput); + } else { + mCallback.onTunerInputSelected(); + } + } } - } - } - }; + }; private final int mInputItemHeight; private final long mShowDurationMillis; @@ -144,23 +143,23 @@ public class SelectInputView extends VerticalGridView implements super(context, attrs, defStyleAttr); setAdapter(new InputListAdapter()); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mTracker = appSingletons.getTracker(); - mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + mTracker = tvSingletons.getTracker(); + mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper(); mComparator = new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper); Resources resources = context.getResources(); mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height); mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration); - mRippleAnimDurationMillis = resources.getInteger( - R.integer.select_input_ripple_anim_duration); + mRippleAnimDurationMillis = + resources.getInteger(R.integer.select_input_ripple_anim_duration); mTextColorPrimary = resources.getColor(R.color.select_input_text_color_primary, null); mTextColorSecondary = resources.getColor(R.color.select_input_text_color_secondary, null); mTextColorDisabled = resources.getColor(R.color.select_input_text_color_disabled, null); - mItemViewForMeasure = LayoutInflater.from(context).inflate( - R.layout.select_input_item, this, false); + mItemViewForMeasure = + LayoutInflater.from(context).inflate(R.layout.select_input_item, this, false); buildInputListAndNotify(); } @@ -199,8 +198,10 @@ public class SelectInputView extends VerticalGridView implements mResetTransitionAlpha = fromEmptyScene; buildInputListAndNotify(); mTvInputManagerHelper.addCallback(mTvInputCallback); - String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ? - mCurrentChannel.getInputId() : null; + String currentInputId = + mCurrentChannel != null && mCurrentChannel.isPassthrough() + ? mCurrentChannel.getInputId() + : null; if (currentInputId != null && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) { // If current input is disabled, the tuner input will be focused. @@ -233,7 +234,8 @@ public class SelectInputView extends VerticalGridView implements @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int height = mInputItemHeight * mInputList.size(); - super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY), + super.onMeasure( + MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } @@ -299,16 +301,14 @@ public class SelectInputView extends VerticalGridView implements != TvInputManager.INPUT_STATE_DISCONNECTED; } - /** - * Sets a callback which receives the notifications of input selection. - */ + /** Sets a callback which receives the notifications of input selection. */ public void setOnInputSelectedCallback(OnInputSelectedCallback callback) { mCallback = callback; } /** - * Sets the current channel. The initial selection will be the input which contains the - * {@code channel}. + * Sets the current channel. The initial selection will be the input which contains the {@code + * channel}. */ public void setCurrentChannel(Channel channel) { mCurrentChannel = channel; @@ -317,8 +317,9 @@ public class SelectInputView extends VerticalGridView implements class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate( - R.layout.select_input_item, parent, false); + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.select_input_item, parent, false); return new ViewHolder(v); } @@ -343,25 +344,29 @@ public class SelectInputView extends VerticalGridView implements holder.secondaryInputLabelView.setVisibility(View.GONE); } - holder.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mSelectedInput = mInputList.get(position); - // The user made a selection. Hide this view after the ripple animation. But - // first, disable focus to avoid any further focus change during the animation. - setFocusable(false); - removeCallbacks(mHideRunnable); - postDelayed(mHideRunnable, mRippleAnimDurationMillis); - } - }); - holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (hasFocus) { - mSelectedInput = mInputList.get(position); - } - } - }); + holder.itemView.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mSelectedInput = mInputList.get(position); + // The user made a selection. Hide this view after the ripple animation. + // But + // first, disable focus to avoid any further focus change during the + // animation. + setFocusable(false); + removeCallbacks(mHideRunnable); + postDelayed(mHideRunnable, mRippleAnimDurationMillis); + } + }); + holder.itemView.setOnFocusChangeListener( + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (hasFocus) { + mSelectedInput = mInputList.get(position); + } + } + }); if (mResetTransitionAlpha) { ViewUtils.setTransitionAlpha(holder.itemView, 1f); @@ -385,18 +390,12 @@ public class SelectInputView extends VerticalGridView implements } } - /** - * A callback interface for the input selection. - */ + /** A callback interface for the input selection. */ public interface OnInputSelectedCallback { - /** - * Called when the tuner input is selected. - */ + /** Called when the tuner input is selected. */ void onTunerInputSelected(); - /** - * Called when the passthrough input is selected. - */ + /** Called when the passthrough input is selected. */ void onPassthroughInputSelected(@NonNull TvInputInfo input); } } diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index 48386698..bb98d974 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -57,37 +57,37 @@ import android.view.SurfaceView; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.Features; import com.android.tv.InputSessionManager; import com.android.tv.InputSessionManager.TvViewSession; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Program; -import com.android.tv.data.ProgramDataManager; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.DurationTimer; -import com.android.tv.util.Debug; +import com.android.tv.TvFeatures; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.BuildConfig; +import com.android.tv.common.CommonConstants; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.Channel; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.util.Debug; +import com.android.tv.common.util.DurationTimer; +import com.android.tv.common.util.PermissionUtils; +import com.android.tv.data.Program; +import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; +import com.android.tv.data.api.Channel; import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.parental.ParentalControlSettings; import com.android.tv.recommendation.NotificationService; -import com.android.tv.util.ImageLoader; import com.android.tv.util.NetworkUtils; -import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - +import com.android.tv.util.images.ImageLoader; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -public class TunableTvView extends FrameLayout implements StreamInfo { +/** Includes the real {@link AppLayerTvView} handling tuning, block and other display events. */ +public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvViewPlayingApi { private static final boolean DEBUG = false; private static final String TAG = "TunableTvView"; @@ -96,20 +96,29 @@ public class TunableTvView extends FrameLayout implements StreamInfo { public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3; public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100; + private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener; + @Retention(RetentionPolicy.SOURCE) @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL}) public @interface BlockScreenType {} + public static final int BLOCK_SCREEN_TYPE_NO_UI = 0; public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1; public static final int BLOCK_SCREEN_TYPE_NORMAL = 2; private static final String PERMISSION_RECEIVE_INPUT_EVENT = - "com.android.tv.permission.RECEIVE_INPUT_EVENT"; + CommonConstants.BASE_PACKAGE + ".permission.RECEIVE_INPUT_EVENT"; @Retention(RetentionPolicy.SOURCE) - @IntDef({ TIME_SHIFT_STATE_NONE, TIME_SHIFT_STATE_PLAY, TIME_SHIFT_STATE_PAUSE, - TIME_SHIFT_STATE_REWIND, TIME_SHIFT_STATE_FAST_FORWARD }) + @IntDef({ + TIME_SHIFT_STATE_NONE, + TIME_SHIFT_STATE_PLAY, + TIME_SHIFT_STATE_PAUSE, + TIME_SHIFT_STATE_REWIND, + TIME_SHIFT_STATE_FAST_FORWARD + }) private @interface TimeShiftState {} + private static final int TIME_SHIFT_STATE_NONE = 0; private static final int TIME_SHIFT_STATE_PLAY = 1; private static final int TIME_SHIFT_STATE_PAUSE = 2; @@ -128,8 +137,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private ContentRatingsManager mContentRatingsManager; private ParentalControlSettings mParentalControlSettings; private ProgramDataManager mProgramDataManager; - @Nullable - private WatchedHistoryManager mWatchedHistoryManager; + @Nullable private WatchedHistoryManager mWatchedHistoryManager; private boolean mStarted; private String mTagetInputId; private TvInputInfo mInputInfo; @@ -182,230 +190,258 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private final ConnectivityManager mConnectivityManager; private final InputSessionManager mInputSessionManager; - private final TvInputCallback mCallback = new TvInputCallback() { - @Override - public void onConnectionFailed(String inputId) { - Log.w(TAG, "Failed to bind an input"); - mTracker.sendInputConnectionFailure(inputId); - Channel channel = mCurrentChannel; - mCurrentChannel = null; - mInputInfo = null; - mCanReceiveInputEvent = false; - if (mOnTuneListener != null) { - // If tune is called inside onTuneFailed, mOnTuneListener will be set to - // a new instance. In order to avoid to clear the new mOnTuneListener, - // we copy mOnTuneListener to l and clear mOnTuneListener before - // calling onTuneFailed. - OnTuneListener listener = mOnTuneListener; - mOnTuneListener = null; - listener.onTuneFailed(channel); - } - } - - @Override - public void onDisconnected(String inputId) { - Log.w(TAG, "Session is released by crash"); - mTracker.sendInputDisconnected(inputId); - Channel channel = mCurrentChannel; - mCurrentChannel = null; - mInputInfo = null; - mCanReceiveInputEvent = false; - if (mOnTuneListener != null) { - OnTuneListener listener = mOnTuneListener; - mOnTuneListener = null; - listener.onUnexpectedStop(channel); - } - } - - @Override - public void onChannelRetuned(String inputId, Uri channelUri) { - if (DEBUG) { - Log.d(TAG, "onChannelRetuned(inputId=" + inputId + ", channelUri=" - + channelUri + ")"); - } - if (mOnTuneListener != null) { - mOnTuneListener.onChannelRetuned(channelUri); - } - } + private final TvInputCallback mCallback = + new TvInputCallback() { + @Override + public void onConnectionFailed(String inputId) { + Log.w(TAG, "Failed to bind an input"); + mTracker.sendInputConnectionFailure(inputId); + Channel channel = mCurrentChannel; + mCurrentChannel = null; + mInputInfo = null; + mCanReceiveInputEvent = false; + if (mOnTuneListener != null) { + // If tune is called inside onTuneFailed, mOnTuneListener will be set to + // a new instance. In order to avoid to clear the new mOnTuneListener, + // we copy mOnTuneListener to l and clear mOnTuneListener before + // calling onTuneFailed. + OnTuneListener listener = mOnTuneListener; + mOnTuneListener = null; + listener.onTuneFailed(channel); + } + } - @Override - public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { - mHasClosedCaption = false; - for (TvTrackInfo track : tracks) { - if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) { - mHasClosedCaption = true; - break; + @Override + public void onDisconnected(String inputId) { + Log.w(TAG, "Session is released by crash"); + mTracker.sendInputDisconnected(inputId); + Channel channel = mCurrentChannel; + mCurrentChannel = null; + mInputInfo = null; + mCanReceiveInputEvent = false; + if (mOnTuneListener != null) { + OnTuneListener listener = mOnTuneListener; + mOnTuneListener = null; + listener.onUnexpectedStop(channel); + } } - } - if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); - } - } - @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (trackId == null) { - // A track is unselected. - if (type == TvTrackInfo.TYPE_VIDEO) { - mVideoWidth = 0; - mVideoHeight = 0; - mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; - mVideoFrameRate = 0f; - mVideoDisplayAspectRatio = 0f; - } else if (type == TvTrackInfo.TYPE_AUDIO) { - mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; + @Override + public void onChannelRetuned(String inputId, Uri channelUri) { + if (DEBUG) { + Log.d( + TAG, + "onChannelRetuned(inputId=" + + inputId + + ", channelUri=" + + channelUri + + ")"); + } + if (mOnTuneListener != null) { + mOnTuneListener.onChannelRetuned(channelUri); + } } - } else { - List<TvTrackInfo> tracks = getTracks(type); - boolean trackFound = false; - if (tracks != null) { + + @Override + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + mHasClosedCaption = false; for (TvTrackInfo track : tracks) { - if (track.getId().equals(trackId)) { - if (type == TvTrackInfo.TYPE_VIDEO) { - mVideoWidth = track.getVideoWidth(); - mVideoHeight = track.getVideoHeight(); - mVideoFormat = Utils.getVideoDefinitionLevelFromSize( - mVideoWidth, mVideoHeight); - mVideoFrameRate = track.getVideoFrameRate(); - if (mVideoWidth <= 0 || mVideoHeight <= 0) { - mVideoDisplayAspectRatio = 0.0f; - } else { - float VideoPixelAspectRatio = - track.getVideoPixelAspectRatio(); - mVideoDisplayAspectRatio = VideoPixelAspectRatio - * mVideoWidth / mVideoHeight; - } - } else if (type == TvTrackInfo.TYPE_AUDIO) { - mAudioChannelCount = track.getAudioChannelCount(); - } - trackFound = true; + if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) { + mHasClosedCaption = true; break; } } + if (mOnTuneListener != null) { + mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + } } - if (!trackFound) { - Log.w(TAG, "Invalid track ID: " + trackId); + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (trackId == null) { + // A track is unselected. + if (type == TvTrackInfo.TYPE_VIDEO) { + mVideoWidth = 0; + mVideoHeight = 0; + mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; + mVideoFrameRate = 0f; + mVideoDisplayAspectRatio = 0f; + } else if (type == TvTrackInfo.TYPE_AUDIO) { + mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN; + } + } else { + List<TvTrackInfo> tracks = getTracks(type); + boolean trackFound = false; + if (tracks != null) { + for (TvTrackInfo track : tracks) { + if (track.getId().equals(trackId)) { + if (type == TvTrackInfo.TYPE_VIDEO) { + mVideoWidth = track.getVideoWidth(); + mVideoHeight = track.getVideoHeight(); + mVideoFormat = + Utils.getVideoDefinitionLevelFromSize( + mVideoWidth, mVideoHeight); + mVideoFrameRate = track.getVideoFrameRate(); + if (mVideoWidth <= 0 || mVideoHeight <= 0) { + mVideoDisplayAspectRatio = 0.0f; + } else { + float VideoPixelAspectRatio = + track.getVideoPixelAspectRatio(); + mVideoDisplayAspectRatio = + VideoPixelAspectRatio + * mVideoWidth + / mVideoHeight; + } + } else if (type == TvTrackInfo.TYPE_AUDIO) { + mAudioChannelCount = track.getAudioChannelCount(); + } + trackFound = true; + break; + } + } + } + if (!trackFound) { + Log.w(TAG, "Invalid track ID: " + trackId); + } + } + if (mOnTuneListener != null) { + mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + } } - } - if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); - } - } - @Override - public void onVideoAvailable(String inputId) { - if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}"); - Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start up of Live TV ends," + - " TunableTvView.onVideoAvailable resets timer"); - long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); - Debug.removeTimer(Debug.TAG_START_UP_TIMER); - if (BuildConfig.ENG && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) { - showAlertDialogForLongStartUp(); - } - mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE; - updateBlockScreenAndMuting(); - if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); - } - } + @Override + public void onVideoAvailable(String inputId) { + if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}"); + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log( + "Start up of Live TV ends," + + " TunableTvView.onVideoAvailable resets timer"); + long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset(); + Debug.removeTimer(Debug.TAG_START_UP_TIMER); + if (BuildConfig.ENG + && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) { + showAlertDialogForLongStartUp(); + } + mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE; + updateBlockScreenAndMuting(); + if (mOnTuneListener != null) { + mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + } + } - private void showAlertDialogForLongStartUp() { - new AlertDialog.Builder(getContext()).setTitle( - getContext().getString(R.string.settings_send_feedback)) - .setMessage("Because the start up time of Live channels is too long," + - " please send feedback") - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = new ApplicationErrorReport(); - report.packageName = report.processName = getContext() - .getApplicationContext().getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_CRASH; - - // Add the crash info to add title of feedback automatically. - ApplicationErrorReport.CrashInfo crash = new - ApplicationErrorReport.CrashInfo(); - crash.exceptionClassName = - "Live TV start up takes long time"; - crash.exceptionMessage = - "The start up time of Live TV is too long"; - report.crashInfo = crash; - - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - getContext().startActivity(intent); - } - }) - .setNegativeButton(android.R.string.no, null) - .show(); - } + private void showAlertDialogForLongStartUp() { + new AlertDialog.Builder(getContext()) + .setTitle(getContext().getString(R.string.settings_send_feedback)) + .setMessage( + "Because the start up time of Live channels is too long," + + " please send feedback") + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick( + DialogInterface dialogInterface, int i) { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = + new ApplicationErrorReport(); + report.packageName = + report.processName = + getContext() + .getApplicationContext() + .getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_CRASH; + + // Add the crash info to add title of feedback + // automatically. + ApplicationErrorReport.CrashInfo crash = + new ApplicationErrorReport.CrashInfo(); + crash.exceptionClassName = + "Live TV start up takes long time"; + crash.exceptionMessage = + "The start up time of Live TV is too long"; + report.crashInfo = crash; + + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + getContext().startActivity(intent); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } - @Override - public void onVideoUnavailable(String inputId, int reason) { - if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "TunableTvView.onVideoUnAvailable reason = (" + reason - + ") and removes timer"); - Debug.removeTimer(Debug.TAG_START_UP_TIMER); - } else { - Debug.getTimer(Debug.TAG_START_UP_TIMER).log( - "TunableTvView.onVideoUnAvailable reason = (" + reason + ")"); - } - mVideoUnavailableReason = reason; - if (closePipIfNeeded()) { - return; - } - updateBlockScreenAndMuting(); - if (mOnTuneListener != null) { - mOnTuneListener.onStreamInfoChanged(TunableTvView.this); - } - switch (reason) { - case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: - case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: - case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: - mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); - default: - // do nothing - } - } + @Override + public void onVideoUnavailable(String inputId, int reason) { + if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING + && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) { + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log( + "TunableTvView.onVideoUnAvailable reason = (" + + reason + + ") and removes timer"); + Debug.removeTimer(Debug.TAG_START_UP_TIMER); + } else { + Debug.getTimer(Debug.TAG_START_UP_TIMER) + .log("TunableTvView.onVideoUnAvailable reason = (" + reason + ")"); + } + mVideoUnavailableReason = reason; + if (closePipIfNeeded()) { + return; + } + updateBlockScreenAndMuting(); + if (mOnTuneListener != null) { + mOnTuneListener.onStreamInfoChanged(TunableTvView.this); + } + switch (reason) { + case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: + mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); + break; + default: + // do nothing + } + } - @Override - public void onContentAllowed(String inputId) { - mBlockedContentRating = null; - updateBlockScreenAndMuting(); - if (mOnTuneListener != null) { - mOnTuneListener.onContentAllowed(); - } - } + @Override + public void onContentAllowed(String inputId) { + mBlockedContentRating = null; + updateBlockScreenAndMuting(); + if (mOnTuneListener != null) { + mOnTuneListener.onContentAllowed(); + } + } - @Override - public void onContentBlocked(String inputId, TvContentRating rating) { - if (rating != null && rating.equals(mBlockedContentRating)) { - return; - } - mBlockedContentRating = rating; - if (closePipIfNeeded()) { - return; - } - updateBlockScreenAndMuting(); - if (mOnTuneListener != null) { - mOnTuneListener.onContentBlocked(); - } - } + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + if (rating != null && rating.equals(mBlockedContentRating)) { + return; + } + mBlockedContentRating = rating; + if (closePipIfNeeded()) { + return; + } + updateBlockScreenAndMuting(); + if (mOnTuneListener != null) { + mOnTuneListener.onContentBlocked(); + } + } - @Override - public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) { - Log.d(TAG, "onTimeShiftStatusChanged: {inputId=" + inputId + ", status=" + status + - "}"); - } - boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; - setTimeShiftAvailable(available); - } - }; + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) { + Log.d( + TAG, + "onTimeShiftStatusChanged: {inputId=" + + inputId + + ", status=" + + status + + "}"); + } + boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE; + setTimeShiftAvailable(available); + } + }; public TunableTvView(Context context) { this(context, null); @@ -423,49 +459,77 @@ public class TunableTvView extends FrameLayout implements StreamInfo { super(context, attrs, defStyleAttr, defStyleRes); inflate(getContext(), R.layout.tunable_tv_view, this); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); if (CommonFeatures.DVR.isEnabled(context)) { - mInputSessionManager = appSingletons.getInputSessionManager(); + mInputSessionManager = tvSingletons.getInputSessionManager(); } else { mInputSessionManager = null; } - mInputManager = appSingletons.getTvInputManagerHelper(); - mConnectivityManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); + mInputManager = tvSingletons.getTvInputManagerHelper(); + mConnectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); - mTracker = appSingletons.getTracker(); + mTracker = tvSingletons.getTracker(); mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen); - mBlockScreenView.addInfoFadeInAnimationListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - adjustBlockScreenSpacingAndText(); - } - }); + mBlockScreenView.addInfoFadeInAnimationListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + adjustBlockScreenSpacingAndText(); + } + }); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); - mTuningImageColorFilter = getResources() - .getColor(R.color.tvview_block_image_color_filter, null); + mTuningImageColorFilter = + getResources().getColor(R.color.tvview_block_image_color_filter, null); mDimScreenView = findViewById(R.id.dim_screen); - mDimScreenView.animate().setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (mActionAfterFade != null) { - mActionAfterFade.run(); - } - } + mDimScreenView + .animate() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mActionAfterFade != null) { + mActionAfterFade.run(); + } + } - @Override - public void onAnimationCancel(Animator animation) { - if (mActionAfterFade != null) { - mActionAfterFade.run(); - } - } - }); + @Override + public void onAnimationCancel(Animator animation) { + if (mActionAfterFade != null) { + mActionAfterFade.run(); + } + } + }); + View placeholder = findViewById(R.id.placeholder); + placeholder.requestFocus(); + findViewById(R.id.channel_up) + .setOnFocusChangeListener( + (v, hasFocus) -> { + if (hasFocus) { + placeholder.requestFocus(); + if (mOnTalkBackDpadKeyListener != null) { + mOnTalkBackDpadKeyListener.onTalkBackDpadKey( + KeyEvent.KEYCODE_DPAD_UP); + } + } + }); + findViewById(R.id.channel_down) + .setOnFocusChangeListener( + (v, hasFocus) -> { + if (hasFocus) { + placeholder.requestFocus(); + if (mOnTalkBackDpadKeyListener != null) { + mOnTalkBackDpadKeyListener.onTalkBackDpadKey( + KeyEvent.KEYCODE_DPAD_DOWN); + } + } + }); } - public void initialize(ProgramDataManager programDataManager, - TvInputManagerHelper tvInputManagerHelper) { + public void initialize( + ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) { mTvView = (AppLayerTvView) findViewById(R.id.tv_view); mProgramDataManager = programDataManager; mInputManagerHelper = tvInputManagerHelper; @@ -482,9 +546,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mStarted = true; } - /** - * Warms up the input to reduce the start time. - */ + /** Warms up the input to reduce the start time. */ public void warmUpInput(String inputId, Uri channelUri) { if (!mStarted && inputId != null && channelUri != null) { if (mTvViewSession != null) { @@ -506,16 +568,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { long duration = mChannelViewTimer.reset(); mTracker.sendChannelViewStop(mCurrentChannel, duration); if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { - mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, - System.currentTimeMillis(), duration); + mWatchedHistoryManager.logChannelViewStop( + mCurrentChannel, System.currentTimeMillis(), duration); } } reset(); } - /** - * Releases the resources. - */ + /** Releases the resources. */ public void release() { if (mInputSessionManager != null) { mInputSessionManager.releaseTvViewSession(mTvViewSession); @@ -523,18 +583,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } } - /** - * Resets TV view. - */ + /** Resets TV view. */ public void reset() { resetInternal(); mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED; updateBlockScreenAndMuting(); } - /** - * Resets TV view to acquire the recording session. - */ + /** Resets TV view to acquire the recording session. */ public void resetByRecording() { resetInternal(); } @@ -560,20 +616,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mWatchedHistoryManager = watchedHistoryManager; } - /** - * Sets if the TunableTvView is under shrunken. - */ + /** Sets if the TunableTvView is under shrunken. */ public void setIsUnderShrunken(boolean isUnderShrunken) { mIsUnderShrunken = isUnderShrunken; } + @Override public boolean isPlaying() { return mStarted; } - /** - * Called when parental control is changed. - */ + /** Called when parental control is changed. */ public void onParentalControlChanged(boolean enabled) { mParentControlEnabled = enabled; if (!enabled) { @@ -586,8 +639,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * Tunes to a channel with the {@code channelId}. * * @param params extra data to send it to TIS and store the data in TIMS. - * @return false, if the TV input is not a proper state to tune to a channel. For example, - * if the state is disconnected or channelId doesn't exist, it returns false. + * @return false, if the TV input is not a proper state to tune to a channel. For example, if + * the state is disconnected or channelId doesn't exist, it returns false. */ public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) { Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo"); @@ -603,24 +656,34 @@ public class TunableTvView extends FrameLayout implements StreamInfo { long duration = mChannelViewTimer.reset(); mTracker.sendChannelViewStop(mCurrentChannel, duration); if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { - mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, - System.currentTimeMillis(), duration); + mWatchedHistoryManager.logChannelViewStop( + mCurrentChannel, System.currentTimeMillis(), duration); } } mOnTuneListener = listener; mCurrentChannel = channel; - boolean tunedByRecommendation = params != null - && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; + boolean tunedByRecommendation = + params != null + && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) + != null; boolean needSurfaceSizeUpdate = false; if (!inputInfo.equals(mInputInfo)) { mTagetInputId = inputInfo.getId(); mInputInfo = inputInfo; - mCanReceiveInputEvent = getContext().getPackageManager().checkPermission( - PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName) + mCanReceiveInputEvent = + getContext() + .getPackageManager() + .checkPermission( + PERMISSION_RECEIVE_INPUT_EVENT, + mInputInfo.getServiceInfo().packageName) == PackageManager.PERMISSION_GRANTED; if (DEBUG) { - Log.d(TAG, "Input \'" + mInputInfo.getId() + "\' can receive input event: " - + mCanReceiveInputEvent); + Log.d( + TAG, + "Input \'" + + mInputInfo.getId() + + "\' can receive input event: " + + mCanReceiveInputEvent); } needSurfaceSizeUpdate = true; } @@ -671,6 +734,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mCurrentChannel = currentChannel; } + @Override public void setStreamVolume(float volume) { if (!mStarted) { throw new IllegalStateException("TvView isn't started"); @@ -683,13 +747,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Sets fixed size for the internal {@link android.view.Surface} of - * {@link android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, - * the {@link android.view.Surface}'s size will be matched to the layout. + * Sets fixed size for the internal {@link android.view.Surface} of {@link + * android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the + * {@link android.view.Surface}'s size will be matched to the layout. * - * Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, - * {@link android.view.SurfaceView} and its underlying window can be misaligned, when the size - * of {@link android.view.SurfaceView} is changed without changing either left position or top + * <p>Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, {@link + * android.view.SurfaceView} and its underlying window can be misaligned, when the size of + * {@link android.view.SurfaceView} is changed without changing either left position or top * position. For detail, please refer the codes of android.view.SurfaceView.updateWindow(). */ public void setFixedSurfaceSize(int width, int height) { @@ -728,10 +792,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo { public interface OnTuneListener { void onTuneFailed(Channel channel); + void onUnexpectedStop(Channel channel); + void onStreamInfoChanged(StreamInfo info); + void onChannelRetuned(Uri channel); + void onContentBlocked(); + void onContentAllowed(); } @@ -759,9 +828,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mVideoFrameRate; } - /** - * Returns displayed aspect ratio (video width / video height * pixel ratio). - */ + /** Returns displayed aspect ratio (video width / video height * pixel ratio). */ @Override public float getVideoDisplayAspectRatio() { return mVideoDisplayAspectRatio; @@ -793,9 +860,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mVideoUnavailableReason; } - /** - * Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. - */ + /** Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. */ private SurfaceView getSurfaceView() { return (SurfaceView) mTvView.getChildAt(0); } @@ -804,6 +869,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mTvView.setOnUnhandledInputEventListener(listener); } + public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) { + mOnTalkBackDpadKeyListener = listener; + } + public void setClosedCaptionEnabled(boolean enabled) { mTvView.setCaptionEnabled(enabled); } @@ -821,16 +890,16 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying - * {@link TvView}, which is the actual view to play live TV videos. + * Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView}, + * which is the actual view to play live TV videos. */ public MarginLayoutParams getTvViewLayoutParams() { return (MarginLayoutParams) mTvView.getLayoutParams(); } /** - * Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying - * {@link TvView}, which is the actual view to play live TV videos. + * Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView}, + * which is the actual view to play live TV videos. */ public void setTvViewLayoutParams(MarginLayoutParams layoutParams) { mTvView.setLayoutParams(layoutParams); @@ -851,16 +920,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return isScreenBlocked() || isContentBlocked(); } - /** - * Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. - */ + /** Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. */ public boolean isScreenBlocked() { return mScreenBlocked; } - /** - * Returns {@code true} if the content is blocked, otherwise {@code false}. - */ + /** Returns {@code true} if the content is blocked, otherwise {@code false}. */ public boolean isContentBlocked() { return mBlockedContentRating != null; } @@ -869,18 +934,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mOnScreenBlockedListener = listener; } - /** - * Returns currently blocked content rating. {@code null} if it's not blocked. - */ + /** Returns currently blocked content rating. {@code null} if it's not blocked. */ @Override public TvContentRating getBlockedContentRating() { return mBlockedContentRating; } /** - * Blocks/unblocks current TV screen and mutes. - * There would be black screen with lock icon in order to show that - * screen block is intended and not an error. + * Blocks/unblocks current TV screen and mutes. There would be black screen with lock icon in + * order to show that screen block is intended and not an error. * * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock. */ @@ -909,8 +971,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { /** * Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the * block screen will not show any description such as a lock icon and a text for the blocked - * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block screen - * will show the description for shrunken tv view (Small icon and short text), and if + * reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block + * screen will show the description for shrunken tv view (Small icon and short text), and if * {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the * description for normal tv view (Big icon and long text). * @@ -925,14 +987,16 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void updateBlockScreen(boolean animation) { mBlockScreenView.endAnimations(); - int blockReason = (mScreenBlocked || mBlockedContentRating != null) - && mParentControlEnabled ? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED + int blockReason = + (mScreenBlocked || mBlockedContentRating != null) && mParentControlEnabled + ? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED : mVideoUnavailableReason; if (blockReason != VIDEO_UNAVAILABLE_REASON_NONE) { mBufferingSpinnerView.setVisibility( blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING - || blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING ? - VISIBLE : GONE); + || blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING + ? VISIBLE + : GONE); if (!animation) { adjustBlockScreenSpacingAndText(); } @@ -959,7 +1023,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) { showImageForTuningIfNeeded(); } else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN - && mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) { + && mCurrentChannel != null + && !mCurrentChannel.isPhysicalTunerChannel()) { mInternetCheckTask = new InternetCheckTask(); mInternetCheckTask.execute(); } @@ -982,8 +1047,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** - * Returns the block screen text corresponding to the current status. - * Note that returning {@code null} value means that the current text should not be changed. + * Returns the block screen text corresponding to the current status. Note that returning {@code + * null} value means that the current text should not be changed. */ private String getBlockScreenText() { // TODO: add a test for this method @@ -1051,7 +1116,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } private boolean closePipIfNeeded() { - if (Features.PICTURE_IN_PICTURE.isEnabled(getContext()) + if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && ((Activity) getContext()).isInPictureInPictureMode() && (mScreenBlocked @@ -1071,8 +1136,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private boolean shouldShowImageForTuning() { if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING - || mScreenBlocked || mBlockedContentRating != null || mCurrentChannel == null - || mIsUnderShrunken || getWidth() == 0 || getWidth() == 0 || !isBundledInput()) { + || mScreenBlocked + || mBlockedContentRating != null + || mCurrentChannel == null + || mIsUnderShrunken + || getWidth() == 0 + || getWidth() == 0 + || !isBundledInput()) { return false; } Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); @@ -1091,7 +1161,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId()); if (currentProgram != null) { - currentProgram.loadPosterArt(getContext(), getWidth(), getHeight(), + currentProgram.loadPosterArt( + getContext(), + getWidth(), + getHeight(), createProgramPosterArtCallback(mCurrentChannel.getId())); } } @@ -1102,16 +1175,19 @@ public class TunableTvView extends FrameLayout implements StreamInfo { TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId); Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId); if (timeMs != null) { - return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource, - input.getTunerCount(), - DateUtils.formatDateTime(getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME)); + return getResources() + .getQuantityString( + R.plurals.tvview_msg_input_no_resource, + input.getTunerCount(), + DateUtils.formatDateTime( + getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME)); } } return null; } private void updateMuteStatus() { - // Workaround: TunerTvInputService uses AC3 pass-through implementation, which disables + // Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables // audio tracks to enforce the mute request. We don't want to send mute request if we are // not going to block the screen to prevent the video jankiness resulted by disabling audio // track before the playback is started. In other way, we should send unmute request before @@ -1119,7 +1195,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { // itself right way when the playback is going to be started, which results the initial // jankiness, too. boolean isBundledInput = isBundledInput(); - if ((isBundledInput || isVideoOrAudioAvailable()) && !mScreenBlocked + if ((isBundledInput || isVideoOrAudioAvailable()) + && !mScreenBlocked && mBlockedContentRating == null) { if (mIsMuted) { mIsMuted = false; @@ -1128,7 +1205,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } else { if (!mIsMuted) { if ((mInputInfo == null || isBundledInput) - && !mScreenBlocked && mBlockedContentRating == null) { + && !mScreenBlocked + && mBlockedContentRating == null) { return; } mIsMuted = true; @@ -1138,8 +1216,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } private boolean isBundledInput() { - return mInputInfo != null && mInputInfo.getType() == TvInputInfo.TYPE_TUNER - && Utils.isBundledInput(mInputInfo.getId()); + return mInputInfo != null + && mInputInfo.getType() == TvInputInfo.TYPE_TUNER + && CommonUtils.isBundledInput(mInputInfo.getId()); } /** Returns true if this view is faded out. */ @@ -1148,52 +1227,58 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } /** Fade out this TunableTvView. Fade out by increasing the dimming. */ - public void fadeOut(int durationMillis, TimeInterpolator interpolator, - final Runnable actionAfterFade) { + public void fadeOut( + int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { mDimScreenView.setAlpha(0f); mDimScreenView.setVisibility(View.VISIBLE); - mDimScreenView.animate() + mDimScreenView + .animate() .alpha(1f) .setDuration(durationMillis) .setInterpolator(interpolator) - .withStartAction(new Runnable() { - @Override - public void run() { - mFadeState = FADING_OUT; - mActionAfterFade = actionAfterFade; - } - }) - .withEndAction(new Runnable() { - @Override - public void run() { - mFadeState = FADED_OUT; - } - }); + .withStartAction( + new Runnable() { + @Override + public void run() { + mFadeState = FADING_OUT; + mActionAfterFade = actionAfterFade; + } + }) + .withEndAction( + new Runnable() { + @Override + public void run() { + mFadeState = FADED_OUT; + } + }); } /** Fade in this TunableTvView. Fade in by decreasing the dimming. */ - public void fadeIn(int durationMillis, TimeInterpolator interpolator, - final Runnable actionAfterFade) { + public void fadeIn( + int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) { mDimScreenView.setAlpha(1f); mDimScreenView.setVisibility(View.VISIBLE); - mDimScreenView.animate() + mDimScreenView + .animate() .alpha(0f) .setDuration(durationMillis) .setInterpolator(interpolator) - .withStartAction(new Runnable() { - @Override - public void run() { - mFadeState = FADING_IN; - mActionAfterFade = actionAfterFade; - } - }) - .withEndAction(new Runnable() { - @Override - public void run() { - mFadeState = FADED_IN; - mDimScreenView.setVisibility(View.GONE); - } - }); + .withStartAction( + new Runnable() { + @Override + public void run() { + mFadeState = FADING_IN; + mActionAfterFade = actionAfterFade; + } + }) + .withEndAction( + new Runnable() { + @Override + public void run() { + mFadeState = FADED_IN; + mDimScreenView.setVisibility(View.GONE); + } + }); } /** Remove the fade effect. */ @@ -1208,6 +1293,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * * @param listener The instance of {@link TimeShiftListener}. */ + @Override public void setTimeShiftListener(TimeShiftListener listener) { mTimeShiftListener = listener; } @@ -1218,20 +1304,22 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mTimeShiftAvailable = isTimeShiftAvailable; if (isTimeShiftAvailable) { - mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { - @Override - public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { - if (mTimeShiftListener != null && mCurrentChannel != null - && mCurrentChannel.getInputId().equals(inputId)) { - mTimeShiftListener.onRecordStartTimeChanged(timeMs); - } - } + mTvView.setTimeShiftPositionCallback( + new TvView.TimeShiftPositionCallback() { + @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (mTimeShiftListener != null + && mCurrentChannel != null + && mCurrentChannel.getInputId().equals(inputId)) { + mTimeShiftListener.onRecordStartTimeChanged(timeMs); + } + } - @Override - public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { - mTimeShiftCurrentPositionMs = timeMs; - } - }); + @Override + public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { + mTimeShiftCurrentPositionMs = timeMs; + } + }); } else { mTvView.setTimeShiftPositionCallback(null); } @@ -1240,16 +1328,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } } - /** - * Returns if the time shift is available for the current channel. - */ + /** Returns if the time shift is available for the current channel. */ + @Override public boolean isTimeShiftAvailable() { return mTimeShiftAvailable; } - /** - * Plays the media, if the current input supports time-shifting. - */ + /** Plays the media, if the current input supports time-shifting. */ + @Override public void timeshiftPlay() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); @@ -1260,9 +1346,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mTvView.timeShiftResume(); } - /** - * Pauses the media, if the current input supports time-shifting. - */ + /** Pauses the media, if the current input supports time-shifting. */ + @Override public void timeshiftPause() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); @@ -1278,6 +1363,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ + @Override public void timeshiftRewind(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); @@ -1297,6 +1383,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x. */ + @Override public void timeshiftFastForward(int speed) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); @@ -1316,6 +1403,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { * * @param timeMs The time in milliseconds to seek to. */ + @Override public void timeshiftSeekTo(long timeMs) { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); @@ -1323,16 +1411,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mTvView.timeShiftSeekTo(timeMs); } - /** - * Returns the current playback position in milliseconds. - */ + /** Returns the current playback position in milliseconds. */ + @Override public long timeshiftGetCurrentPositionMs() { if (!isTimeShiftAvailable()) { throw new IllegalStateException("Time-shift is not supported for the current channel"); } if (DEBUG) { - Log.d(TAG, "timeshiftGetCurrentPositionMs: current position =" - + Utils.toTimeString(mTimeShiftCurrentPositionMs)); + Log.d( + TAG, + "timeshiftGetCurrentPositionMs: current position =" + + Utils.toTimeString(mTimeShiftCurrentPositionMs)); } return mTimeShiftCurrentPositionMs; } @@ -1342,43 +1431,30 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) { @Override public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) { - if (posterArt == null || getCurrentChannel() == null + if (posterArt == null + || getCurrentChannel() == null || channelId != getCurrentChannel().getId() || !shouldShowImageForTuning()) { return; } Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt); - drawablePosterArt.mutate().setColorFilter( - mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER); + drawablePosterArt + .mutate() + .setColorFilter(mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER); view.setBackgroundImage(drawablePosterArt); } }; } - /** - * Used to receive the time-shift events. - */ - public static abstract class TimeShiftListener { - /** - * Called when the availability of the time-shift for the current channel has been changed. - * It should be guaranteed that this is called only when the availability is really changed. - */ - public abstract void onAvailabilityChanged(); + /** Listens for dpad actions that are otherwise trapped by talkback */ + public interface OnTalkBackDpadKeyListener { - /** - * Called when the record start time has been changed. - * This is not called when the recorded programs is played. - */ - public abstract void onRecordStartTimeChanged(long recordStartTimeMs); + void onTalkBackDpadKey(int keycode); } - /** - * A listener which receives the notification when the screen is blocked/unblocked. - */ - public static abstract class OnScreenBlockingChangedListener { - /** - * Called when the screen is blocked/unblocked. - */ + /** A listener which receives the notification when the screen is blocked/unblocked. */ + public abstract static class OnScreenBlockingChangedListener { + /** Called when the screen is blocked/unblocked. */ public abstract void onScreenBlockingChanged(boolean blocked); } @@ -1391,8 +1467,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override protected void onPostExecute(Boolean networkAvailable) { mInternetCheckTask = null; - if (!networkAvailable && isAttachedToWindow() - && !mScreenBlocked && mBlockedContentRating == null + if (!networkAvailable + && isAttachedToWindow() + && !mScreenBlocked + && mBlockedContentRating == null && mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) { mBlockScreenView.setIconVisibility(true); mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud); diff --git a/src/com/android/tv/ui/TunableTvViewPlayingApi.java b/src/com/android/tv/ui/TunableTvViewPlayingApi.java new file mode 100644 index 00000000..3f19b61f --- /dev/null +++ b/src/com/android/tv/ui/TunableTvViewPlayingApi.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.ui; + +/** API to play pause and set the volume of a TunableTvView */ +public interface TunableTvViewPlayingApi { + + boolean isPlaying(); + + void setStreamVolume(float volume); + + void setTimeShiftListener(TimeShiftListener listener); + + boolean isTimeShiftAvailable(); + + void timeshiftPlay(); + + void timeshiftPause(); + + void timeshiftRewind(int speed); + + void timeshiftFastForward(int speed); + + void timeshiftSeekTo(long timeMs); + + long timeshiftGetCurrentPositionMs(); + + /** Used to receive the time-shift events. */ + abstract class TimeShiftListener { + /** + * Called when the availability of the time-shift for the current channel has been changed. + * It should be guaranteed that this is called only when the availability is really changed. + */ + public abstract void onAvailabilityChanged(); + + /** + * Called when the record start time has been changed. This is not called when the recorded + * programs is played. + */ + public abstract void onRecordStartTimeChanged(long recordStartTimeMs); + } +} diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index 9324742e..222fcb3a 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -32,15 +32,14 @@ import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import com.android.tv.ChannelTuner; import com.android.tv.MainActivity; import com.android.tv.MainActivity.KeyHandlerResultType; import com.android.tv.R; import com.android.tv.TimeShiftManager; -import com.android.tv.TvApplication; import com.android.tv.TvOptionsManager; +import com.android.tv.TvSingletons; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; @@ -69,7 +68,6 @@ import com.android.tv.ui.TvTransitionManager.SceneType; import com.android.tv.ui.sidepanel.SideFragmentManager; import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment; import com.android.tv.util.TvInputManagerHelper; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -79,122 +77,111 @@ import java.util.List; import java.util.Queue; import java.util.Set; -/** - * A class responsible for the life cycle and event handling of the pop-ups over TV view. - */ +/** A class responsible for the life cycle and event handling of the pop-ups over TV view. */ @UiThread -public class TvOverlayManager { +public class TvOverlayManager implements AccessibilityStateChangeListener { private static final String TAG = "TvOverlayManager"; private static final boolean DEBUG = false; private static final String INTRO_TRACKER_LABEL = "Intro dialog"; @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {FLAG_HIDE_OVERLAYS_DEFAULT, FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, - FLAG_HIDE_OVERLAYS_KEEP_SCENE, FLAG_HIDE_OVERLAYS_KEEP_DIALOG, - FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, - FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU, - FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT}) + @IntDef( + flag = true, + value = { + FLAG_HIDE_OVERLAYS_DEFAULT, + FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, + FLAG_HIDE_OVERLAYS_KEEP_SCENE, + FLAG_HIDE_OVERLAYS_KEEP_DIALOG, + FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, + FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, + FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, + FLAG_HIDE_OVERLAYS_KEEP_MENU, + FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT + } + ) private @interface HideOverlayFlag {} // FLAG_HIDE_OVERLAYs must be bitwise exclusive. - public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000; - public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION = 0b000000010; - public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE = 0b000000100; - public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG = 0b000001000; - public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b000010000; + public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000; + public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION = 0b000000010; + public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE = 0b000000100; + public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG = 0b000001000; + public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b000010000; public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b000100000; - public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b001000000; - public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b010000000; - public static final int FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT = 0b100000000; + public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b001000000; + public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b010000000; + public static final int FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT = 0b100000000; private static final int MSG_OVERLAY_CLOSED = 1000; @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {OVERLAY_TYPE_NONE, OVERLAY_TYPE_MENU, OVERLAY_TYPE_SIDE_FRAGMENT, - OVERLAY_TYPE_DIALOG, OVERLAY_TYPE_GUIDE, OVERLAY_TYPE_SCENE_CHANNEL_BANNER, - OVERLAY_TYPE_SCENE_INPUT_BANNER, OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH, - OVERLAY_TYPE_SCENE_SELECT_INPUT, OVERLAY_TYPE_FRAGMENT}) + @IntDef( + flag = true, + value = { + OVERLAY_TYPE_NONE, + OVERLAY_TYPE_MENU, + OVERLAY_TYPE_SIDE_FRAGMENT, + OVERLAY_TYPE_DIALOG, + OVERLAY_TYPE_GUIDE, + OVERLAY_TYPE_SCENE_CHANNEL_BANNER, + OVERLAY_TYPE_SCENE_INPUT_BANNER, + OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH, + OVERLAY_TYPE_SCENE_SELECT_INPUT, + OVERLAY_TYPE_FRAGMENT + } + ) private @interface TvOverlayType {} // OVERLAY_TYPEs must be bitwise exclusive. - /** - * The overlay type which indicates that there are no overlays. - */ - private static final int OVERLAY_TYPE_NONE = 0b000000000; - /** - * The overlay type for menu. - */ - private static final int OVERLAY_TYPE_MENU = 0b000000001; - /** - * The overlay type for the side fragment. - */ - private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b000000010; - /** - * The overlay type for dialog fragment. - */ - private static final int OVERLAY_TYPE_DIALOG = 0b000000100; - /** - * The overlay type for program guide. - */ - private static final int OVERLAY_TYPE_GUIDE = 0b000001000; - /** - * The overlay type for channel banner. - */ - private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b000010000; - /** - * The overlay type for input banner. - */ - private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b000100000; - /** - * The overlay type for keypad channel switch view. - */ + /** The overlay type which indicates that there are no overlays. */ + private static final int OVERLAY_TYPE_NONE = 0b000000000; + /** The overlay type for menu. */ + private static final int OVERLAY_TYPE_MENU = 0b000000001; + /** The overlay type for the side fragment. */ + private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b000000010; + /** The overlay type for dialog fragment. */ + private static final int OVERLAY_TYPE_DIALOG = 0b000000100; + /** The overlay type for program guide. */ + private static final int OVERLAY_TYPE_GUIDE = 0b000001000; + /** The overlay type for channel banner. */ + private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b000010000; + /** The overlay type for input banner. */ + private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b000100000; + /** The overlay type for keypad channel switch view. */ private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b001000000; - /** - * The overlay type for select input view. - */ - private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b010000000; - /** - * The overlay type for fragment other than the side fragment and dialog fragment. - */ - private static final int OVERLAY_TYPE_FRAGMENT = 0b100000000; + /** The overlay type for select input view. */ + private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b010000000; + /** The overlay type for fragment other than the side fragment and dialog fragment. */ + private static final int OVERLAY_TYPE_FRAGMENT = 0b100000000; // Used for the padded print of the overlay type. private static final int NUM_OVERLAY_TYPES = 9; @Retention(RetentionPolicy.SOURCE) - @IntDef({UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE, - UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, - UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK, - UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO}) + @IntDef({ + UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, + UPDATE_CHANNEL_BANNER_REASON_TUNE, + UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, + UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, + UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK, + UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO + }) private @interface ChannelBannerUpdateReason {} - /** - * Updates channel banner because the channel banner is forced to show. - */ + /** Updates channel banner because the channel banner is forced to show. */ public static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1; - /** - * Updates channel banner because of tuning. - */ + /** Updates channel banner because of tuning. */ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2; - /** - * Updates channel banner because of fast tuning. - */ + /** Updates channel banner because of fast tuning. */ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3; - /** - * Updates channel banner because of info updating. - */ + /** Updates channel banner because of info updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4; - /** - * Updates channel banner because the current watched channel is locked or unlocked. - */ + /** Updates channel banner because the current watched channel is locked or unlocked. */ public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; - /** - * Updates channel banner because of stream info updating. - */ + /** Updates channel banner because of stream info updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6; private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources"; private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources"; private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>(); + static { AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG); @@ -232,14 +219,20 @@ public class TvOverlayManager { private OnBackStackChangedListener mOnBackStackChangedListener; - public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, - TunableTvView tvView, TvOptionsManager optionsManager, - KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, - InputBannerView inputBannerView, SelectInputView selectInputView, - ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment) { + public TvOverlayManager( + MainActivity mainActivity, + ChannelTuner channelTuner, + TunableTvView tvView, + TvOptionsManager optionsManager, + KeypadChannelSwitchView keypadChannelSwitchView, + ChannelBannerView channelBannerView, + InputBannerView inputBannerView, + SelectInputView selectInputView, + ViewGroup sceneContainer, + ProgramGuideSearchFragment searchFragment) { mMainActivity = mainActivity; mChannelTuner = channelTuner; - ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity); + TvSingletons singletons = TvSingletons.getSingletons(mainActivity); mChannelDataManager = singletons.getChannelDataManager(); mInputManager = singletons.getTvInputManagerHelper(); mTvView = tvView; @@ -248,115 +241,144 @@ public class TvOverlayManager { mSelectInputView = selectInputView; mSearchFragment = searchFragment; mTracker = singletons.getTracker(); - mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer, - channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView); - mTransitionManager.setListener(new TvTransitionManager.Listener() { - @Override - public void onSceneChanged(int fromScene, int toScene) { - // Call onOverlayOpened first so that the listener can know that a new scene - // will be opened when the onOverlayClosed is called. - if (toScene != TvTransitionManager.SCENE_TYPE_EMPTY) { - onOverlayOpened(convertSceneToOverlayType(toScene)); - } - if (fromScene != TvTransitionManager.SCENE_TYPE_EMPTY) { - onOverlayClosed(convertSceneToOverlayType(fromScene)); - } - } - }); - // Menu - MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); - mMenu = new Menu(mainActivity, tvView, optionsManager, menuView, - new MenuRowFactory(mainActivity, tvView), - new Menu.OnMenuVisibilityChangeListener() { + mTransitionManager = + new TvTransitionManager( + mainActivity, + sceneContainer, + channelBannerView, + inputBannerView, + mKeypadChannelSwitchView, + selectInputView); + mTransitionManager.setListener( + new TvTransitionManager.Listener() { @Override - public void onMenuVisibilityChange(boolean visible) { - if (visible) { - onOverlayOpened(OVERLAY_TYPE_MENU); - } else { - onOverlayClosed(OVERLAY_TYPE_MENU); + public void onSceneChanged(int fromScene, int toScene) { + // Call onOverlayOpened first so that the listener can know that a new scene + // will be opened when the onOverlayClosed is called. + if (toScene != TvTransitionManager.SCENE_TYPE_EMPTY) { + onOverlayOpened(convertSceneToOverlayType(toScene)); + } + if (fromScene != TvTransitionManager.SCENE_TYPE_EMPTY) { + onOverlayClosed(convertSceneToOverlayType(fromScene)); } } }); + // Menu + MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); + mMenu = + new Menu( + mainActivity, + tvView, + optionsManager, + menuView, + new MenuRowFactory(mainActivity, tvView), + new Menu.OnMenuVisibilityChangeListener() { + @Override + public void onMenuVisibilityChange(boolean visible) { + if (visible) { + onOverlayOpened(OVERLAY_TYPE_MENU); + } else { + onOverlayClosed(OVERLAY_TYPE_MENU); + } + } + }); mMenu.setChannelTuner(mChannelTuner); // Side Fragment - mSideFragmentManager = new SideFragmentManager(mainActivity, + mSideFragmentManager = + new SideFragmentManager( + mainActivity, + new Runnable() { + @Override + public void run() { + onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); + hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); + } + }, + new Runnable() { + @Override + public void run() { + showChannelBannerIfHiddenBySideFragment(); + onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); + } + }); + // Program Guide + Runnable preShowRunnable = new Runnable() { @Override public void run() { - onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); - hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); + onOverlayOpened(OVERLAY_TYPE_GUIDE); } - }, + }; + Runnable postHideRunnable = new Runnable() { @Override public void run() { - showChannelBannerIfHiddenBySideFragment(); - onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); + onOverlayClosed(OVERLAY_TYPE_GUIDE); } - }); - // Program Guide - Runnable preShowRunnable = new Runnable() { - @Override - public void run() { - onOverlayOpened(OVERLAY_TYPE_GUIDE); - } - }; - Runnable postHideRunnable = new Runnable() { - @Override - public void run() { - onOverlayClosed(OVERLAY_TYPE_GUIDE); - } - }; - DvrDataManager dvrDataManager = CommonFeatures.DVR.isEnabled(mainActivity) - ? singletons.getDvrDataManager() : null; - mProgramGuide = new ProgramGuide(mainActivity, channelTuner, - singletons.getTvInputManagerHelper(), mChannelDataManager, - singletons.getProgramDataManager(), dvrDataManager, - singletons.getDvrScheduleManager(), singletons.getTracker(), preShowRunnable, - postHideRunnable); - mMainActivity.addOnActionClickListener(new OnActionClickListener() { - @Override - public boolean onActionClick(String category, int id, Bundle params) { - switch (category) { - case SetupSourcesFragment.ACTION_CATEGORY: - switch (id) { - case SetupMultiPaneFragment.ACTION_DONE: - closeSetupFragment(true); - return true; - case SetupSourcesFragment.ACTION_ONLINE_STORE: - mMainActivity.showMerchantCollection(); - return true; - case SetupSourcesFragment.ACTION_SETUP_INPUT: { - String inputId = params.getString( - SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - mMainActivity.startSetupActivity(input, true); - return true; - } - } - break; - case NewSourcesFragment.ACTION_CATEOGRY: - switch (id) { - case NewSourcesFragment.ACTION_SETUP: - closeNewSourcesFragment(false); - showSetupFragment(); - return true; - case NewSourcesFragment.ACTION_SKIP: - // Don't remove the fragment because new fragment will be replaced - // with this fragment. - closeNewSourcesFragment(true); - return true; + }; + DvrDataManager dvrDataManager = + CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null; + mProgramGuide = + new ProgramGuide( + mainActivity, + channelTuner, + singletons.getTvInputManagerHelper(), + mChannelDataManager, + singletons.getProgramDataManager(), + dvrDataManager, + singletons.getDvrScheduleManager(), + singletons.getTracker(), + preShowRunnable, + postHideRunnable); + mMainActivity.addOnActionClickListener( + new OnActionClickListener() { + @Override + public boolean onActionClick(String category, int id, Bundle params) { + switch (category) { + case SetupSourcesFragment.ACTION_CATEGORY: + switch (id) { + case SetupMultiPaneFragment.ACTION_DONE: + closeSetupFragment(true); + return true; + case SetupSourcesFragment.ACTION_ONLINE_STORE: + mMainActivity.showMerchantCollection(); + return true; + case SetupSourcesFragment.ACTION_SETUP_INPUT: + { + String inputId = + params.getString( + SetupSourcesFragment + .ACTION_PARAM_KEY_INPUT_ID); + TvInputInfo input = + mInputManager.getTvInputInfo(inputId); + mMainActivity.startSetupActivity(input, true); + return true; + } + } + break; + case NewSourcesFragment.ACTION_CATEOGRY: + switch (id) { + case NewSourcesFragment.ACTION_SETUP: + closeNewSourcesFragment(false); + showSetupFragment(); + return true; + case NewSourcesFragment.ACTION_SKIP: + // Don't remove the fragment because new fragment will be + // replaced + // with this fragment. + closeNewSourcesFragment(true); + return true; + } + break; } - break; - } - return false; - } - }); + return false; + } + }); } /** - * A method to release all the allocated resources or unregister listeners. - * This is called from {@link MainActivity#onDestroy}. + * A method to release all the allocated resources or unregister listeners. This is called from + * {@link MainActivity#onDestroy}. */ public void release() { mMenu.release(); @@ -366,30 +388,22 @@ public class TvOverlayManager { } } - /** - * Returns the instance of {@link Menu}. - */ + /** Returns the instance of {@link Menu}. */ public Menu getMenu() { return mMenu; } - /** - * Returns the instance of {@link SideFragmentManager}. - */ + /** Returns the instance of {@link SideFragmentManager}. */ public SideFragmentManager getSideFragmentManager() { return mSideFragmentManager; } - /** - * Returns the currently opened dialog. - */ + /** Returns the currently opened dialog. */ public SafeDismissDialogFragment getCurrentDialog() { return mCurrentDialog; } - /** - * Checks whether the setup fragment is active or not. - */ + /** Checks whether the setup fragment is active or not. */ public boolean isSetupFragmentActive() { // "getSetupSourcesFragment() != null" doesn't return the correct state. That's because, // when we call showSetupFragment(), we need to put off showing the fragment until the side @@ -402,9 +416,7 @@ public class TvOverlayManager { return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_SETUP_SOURCES); } - /** - * Checks whether the new sources fragment is active or not. - */ + /** Checks whether the new sources fragment is active or not. */ public boolean isNewSourcesFragmentActive() { // See the comment in "isSetupFragmentActive". return mNewSourcesFragmentActive; @@ -414,25 +426,19 @@ public class TvOverlayManager { return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_NEW_SOURCES); } - /** - * Returns the instance of {@link ProgramGuide}. - */ + /** Returns the instance of {@link ProgramGuide}. */ public ProgramGuide getProgramGuide() { return mProgramGuide; } - /** - * Shows the main menu. - */ + /** Shows the main menu. */ public void showMenu(@MenuShowReason int reason) { if (mChannelTuner != null && mChannelTuner.areAllChannelsLoaded()) { mMenu.show(reason); } } - /** - * Shows the play controller of the menu if the playback is paused. - */ + /** Shows the play controller of the menu if the playback is paused. */ public boolean showMenuWithTimeShiftPauseIfNeeded() { if (mMainActivity.getTimeShiftManager().isPaused()) { showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE); @@ -441,16 +447,17 @@ public class TvOverlayManager { return false; } - /** - * Shows the given dialog. - */ - public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, - boolean keepSidePanelHistory) { + /** Shows the given dialog. */ + public void showDialogFragment( + String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory) { showDialogFragment(tag, dialog, keepSidePanelHistory, false); } - public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, - boolean keepSidePanelHistory, boolean keepProgramGuide) { + public void showDialogFragment( + String tag, + SafeDismissDialogFragment dialog, + boolean keepSidePanelHistory, + boolean keepProgramGuide) { int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG; if (keepSidePanelHistory) { flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY; @@ -466,8 +473,8 @@ public class TvOverlayManager { // Do not open two dialogs at the same time. if (mCurrentDialog != null) { - mPendingDialogActionQueue.offer(new PendingDialogAction(tag, dialog, - keepSidePanelHistory, keepProgramGuide)); + mPendingDialogActionQueue.offer( + new PendingDialogAction(tag, dialog, keepSidePanelHistory, keepProgramGuide)); return; } @@ -492,16 +499,17 @@ public class TvOverlayManager { // When the side panel is closing, it closes all the fragments, so the new fragment // should be opened after the side fragment becomes invisible. final FragmentManager manager = mMainActivity.getFragmentManager(); - mOnBackStackChangedListener = new OnBackStackChangedListener() { - @Override - public void onBackStackChanged() { - if (manager.getBackStackEntryCount() == 0) { - manager.removeOnBackStackChangedListener(this); - mOnBackStackChangedListener = null; - runnable.run(); - } - } - }; + mOnBackStackChangedListener = + new OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + if (manager.getBackStackEntryCount() == 0) { + manager.removeOnBackStackChangedListener(this); + mOnBackStackChangedListener = null; + runnable.run(); + } + } + }; manager.addOnBackStackChangedListener(mOnBackStackChangedListener); } else { runnable.run(); @@ -511,46 +519,54 @@ public class TvOverlayManager { private void showFragment(final Fragment fragment, final String tag) { hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); onOverlayOpened(OVERLAY_TYPE_FRAGMENT); - runAfterSideFragmentsAreClosed(new Runnable() { - @Override - public void run() { - if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")"); - mMainActivity.getFragmentManager().beginTransaction() - .replace(R.id.fragment_container, fragment, tag).commit(); - } - }); + runAfterSideFragmentsAreClosed( + new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")"); + mMainActivity + .getFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, fragment, tag) + .commit(); + } + }); } private void closeFragment(String fragmentTagToRemove) { if (DEBUG) Log.d(TAG, "closeFragment(" + fragmentTagToRemove + ")"); onOverlayClosed(OVERLAY_TYPE_FRAGMENT); if (fragmentTagToRemove != null) { - Fragment fragmentToRemove = mMainActivity.getFragmentManager() - .findFragmentByTag(fragmentTagToRemove); + Fragment fragmentToRemove = + mMainActivity.getFragmentManager().findFragmentByTag(fragmentTagToRemove); if (fragmentToRemove == null) { // If the fragment has not been added to the fragment manager yet, just remove the // listener not to add the fragment. This is needed because the side fragment is // closed asynchronously. - mMainActivity.getFragmentManager().removeOnBackStackChangedListener( - mOnBackStackChangedListener); + mMainActivity + .getFragmentManager() + .removeOnBackStackChangedListener(mOnBackStackChangedListener); mOnBackStackChangedListener = null; } else { - mMainActivity.getFragmentManager().beginTransaction().remove(fragmentToRemove) + mMainActivity + .getFragmentManager() + .beginTransaction() + .remove(fragmentToRemove) .commit(); } } } - /** - * Shows setup dialog. - */ + /** Shows setup dialog. */ public void showSetupFragment() { if (DEBUG) Log.d(TAG, "showSetupFragment"); mSetupFragmentActive = true; SetupSourcesFragment setupFragment = new SetupSourcesFragment(); - setupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION - | SetupFragment.FRAGMENT_REENTER_TRANSITION); + setupFragment.enableFragmentTransition( + SetupFragment.FRAGMENT_ENTER_TRANSITION + | SetupFragment.FRAGMENT_EXIT_TRANSITION + | SetupFragment.FRAGMENT_RETURN_TRANSITION + | SetupFragment.FRAGMENT_REENTER_TRANSITION); setupFragment.setFragmentTransition(SetupFragment.FRAGMENT_EXIT_TRANSITION, Gravity.END); showFragment(setupFragment, FRAGMENT_TAG_SETUP_SOURCES); } @@ -569,9 +585,7 @@ public class TvOverlayManager { } } - /** - * Shows new sources dialog. - */ + /** Shows new sources dialog. */ public void showNewSourcesFragment() { if (DEBUG) Log.d(TAG, "showNewSourcesFragment"); mNewSourcesFragmentActive = true; @@ -588,43 +602,36 @@ public class TvOverlayManager { closeFragment(removeFragment ? FRAGMENT_TAG_NEW_SOURCES : null); } - /** - * Shows DVR manager. - */ + /** Shows DVR manager. */ public void showDvrManager() { Intent intent = new Intent(mMainActivity, DvrBrowseActivity.class); mMainActivity.startActivity(intent); } - /** - * Shows intro dialog. - */ + /** Shows intro dialog. */ public void showIntroDialog() { - if (DEBUG) Log.d(TAG,"showIntroDialog"); - showDialogFragment(FullscreenDialogFragment.DIALOG_TAG, + if (DEBUG) Log.d(TAG, "showIntroDialog"); + showDialogFragment( + FullscreenDialogFragment.DIALOG_TAG, FullscreenDialogFragment.newInstance(R.layout.intro_dialog, INTRO_TRACKER_LABEL), false); } - /** - * Shows recently watched dialog. - */ + /** Shows recently watched dialog. */ public void showRecentlyWatchedDialog() { - showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, - new RecentlyWatchedDialogFragment(), false); + showDialogFragment( + RecentlyWatchedDialogFragment.DIALOG_TAG, + new RecentlyWatchedDialogFragment(), + false); } - /** - * Shows DVR history dialog. - */ + /** Shows DVR history dialog. */ public void showDvrHistoryDialog() { - showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG, - new DvrHistoryDialogFragment(), false); + showDialogFragment( + DvrHistoryDialogFragment.DIALOG_TAG, new DvrHistoryDialogFragment(), false); } - /** - * Shows banner view. - */ + /** Shows banner view. */ public void showBanner() { mTransitionManager.goToChannelBannerScene(); } @@ -636,33 +643,28 @@ public class TvOverlayManager { */ public void showKeypadChannelSwitch(int keyCode) { if (mChannelTuner.areAllChannelsLoaded()) { - hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); mTransitionManager.goToKeypadChannelSwitchScene(); mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0); } } - /** - * Shows select input view. - */ + /** Shows select input view. */ public void showSelectInputView() { hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE); mTransitionManager.goToSelectInputScene(); } - /** - * Initializes animators if animators are not initialized yet. - */ + /** Initializes animators if animators are not initialized yet. */ public void initAnimatorIfNeeded() { mTransitionManager.initIfNeeded(); } - /** - * It is called when a SafeDismissDialogFragment is destroyed. - */ + /** It is called when a SafeDismissDialogFragment is destroyed. */ public void onDialogDestroyed() { mCurrentDialog = null; PendingDialogAction action = mPendingDialogActionQueue.poll(); @@ -673,16 +675,15 @@ public class TvOverlayManager { } } - /** - * Shows the program guide. - */ + /** Shows the program guide. */ public void showProgramGuide() { - mProgramGuide.show(new Runnable() { - @Override - public void run() { - hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); - } - }); + mProgramGuide.show( + new Runnable() { + @Override + public void run() { + hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); + } + }); } /** @@ -700,9 +701,7 @@ public class TvOverlayManager { } } - /** - * Sets blocking content rating of the currently playing TV channel. - */ + /** Sets blocking content rating of the currently playing TV channel. */ public void setBlockingContentRating(TvContentRating rating) { if (!mMainActivity.isChannelChangeKeyDownReceived()) { mChannelBannerView.setBlockingContentRating(rating); @@ -710,9 +709,11 @@ public class TvOverlayManager { } } - /** - * Hides all the opened overlays according to the flags. - */ + public boolean isOverlayOpened() { + return mOpenedOverlays != OVERLAY_TYPE_NONE; + } + + /** Hides all the opened overlays according to the flags. */ // TODO: Add test for this method. public void hideOverlays(@HideOverlayFlag int flags) { if (mMainActivity.needToKeepSetupScreenWhenHidingOverlay()) { @@ -780,9 +781,17 @@ public class TvOverlayManager { } } + @Override + public void onAccessibilityStateChanged(boolean enabled) { + // Propagate this to all elements that need it + mChannelBannerView.onAccessibilityStateChanged(enabled); + mProgramGuide.onAccessibilityStateChanged(enabled); + mSideFragmentManager.onAccessibilityStateChanged(enabled); + } + /** - * Returns true, if a main view needs to hide informational text. Specifically, when overlay - * UIs except banner is shown, the informational text needs to be hidden for clean UI. + * Returns true, if a main view needs to hide informational text. Specifically, when overlay UIs + * except banner is shown, the informational text needs to be hidden for clean UI. */ public boolean needHideTextOnMainView() { return mSideFragmentManager.isActive() @@ -793,11 +802,9 @@ public class TvOverlayManager { || mNewSourcesFragmentActive; } - /** - * Updates and shows channel banner if it's needed. - */ + /** Updates and shows channel banner if it's needed. */ public void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) { - if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); + if (DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); if (mMainActivity.isChannelChangeKeyDownReceived() && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { @@ -826,8 +833,9 @@ public class TvOverlayManager { } } else if (mTvView.isScreenBlocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; - } else if (mTvView.isContentBlocked() || (mMainActivity.getParentalControlSettings() - .isParentalControlsEnabled() && !mTvView.isVideoOrAudioAvailable())) { + } else if (mTvView.isContentBlocked() + || (mMainActivity.getParentalControlSettings().isParentalControlsEnabled() + && !mTvView.isVideoOrAudioAvailable())) { // If the parental control is enabled, do not show the program detail until the // video becomes available. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; @@ -843,19 +851,22 @@ public class TvOverlayManager { // If parental control is enabled, we shows program description when the video is // available, instead of tuning. Therefore we need to check it here if the program // description is previously hidden by parental control. - if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL && - lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { + if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL + && lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { mChannelBannerView.updateViews(false); } } else { - mChannelBannerView.updateViews(reason == UPDATE_CHANNEL_BANNER_REASON_TUNE - || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); + mChannelBannerView.updateViews( + reason == UPDATE_CHANNEL_BANNER_REASON_TUNE + || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); } } - boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW - || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE - || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); - if (needToShowBanner && !mMainActivity.willShowOverlayUiWhenResume() + boolean needToShowBanner = + (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW + || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE + || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); + if (needToShowBanner + && !mMainActivity.willShowOverlayUiWhenResume() && getCurrentDialog() == null && !isSetupFragmentActive() && !isNewSourcesFragmentActive()) { @@ -870,7 +881,8 @@ public class TvOverlayManager { } } - @TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) { + @TvOverlayType + private int convertSceneToOverlayType(@SceneType int sceneType) { switch (sceneType) { case TvTransitionManager.SCENE_TYPE_CHANNEL_BANNER: return OVERLAY_TYPE_SCENE_CHANNEL_BANNER; @@ -940,13 +952,13 @@ public class TvOverlayManager { } private boolean isOnlyBannerOrNoneOpened() { - return (mOpenedOverlays & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER - & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) == 0; + return (mOpenedOverlays + & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER + & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) + == 0; } - /** - * Runs a given {@code action} after all the overlays are closed. - */ + /** Runs a given {@code action} after all the overlays are closed. */ public void runAfterOverlaysAreClosed(Runnable action) { if (canExecuteCloseAction()) { action.run(); @@ -955,9 +967,7 @@ public class TvOverlayManager { } } - /** - * Handles the onUserInteraction event of the {@link MainActivity}. - */ + /** Handles the onUserInteraction event of the {@link MainActivity}. */ public void onUserInteraction() { if (mSideFragmentManager.isActive()) { mSideFragmentManager.scheduleHideAll(); @@ -968,10 +978,9 @@ public class TvOverlayManager { } } - /** - * Handles the onKeyDown event of the {@link MainActivity}. - */ - @KeyHandlerResultType public int onKeyDown(int keyCode, KeyEvent event) { + /** Handles the onKeyDown event of the {@link MainActivity}. */ + @KeyHandlerResultType + public int onKeyDown(int keyCode, KeyEvent event) { if (mCurrentDialog != null) { // Consumes the keys while a Dialog is creating. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; @@ -981,31 +990,34 @@ public class TvOverlayManager { // Consumes the keys which may trigger system's default music player. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } - if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive() - || mSetupFragmentActive || mNewSourcesFragmentActive) { + if (mMenu.isActive() + || mSideFragmentManager.isActive() + || mProgramGuide.isActive() + || mSetupFragmentActive + || mNewSourcesFragmentActive) { return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mTransitionManager.isKeypadChannelSwitchActive()) { - return mKeypadChannelSwitchView.onKeyDown(keyCode, event) ? - MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED + return mKeypadChannelSwitchView.onKeyDown(keyCode, event) + ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mTransitionManager.isSelectInputActive()) { - return mSelectInputView.onKeyDown(keyCode, event) ? - MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED + return mSelectInputView.onKeyDown(keyCode, event) + ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH; } - /** - * Handles the onKeyUp event of the {@link MainActivity}. - */ - @KeyHandlerResultType public int onKeyUp(int keyCode, KeyEvent event) { + /** Handles the onKeyUp event of the {@link MainActivity}. */ + @KeyHandlerResultType + public int onKeyUp(int keyCode, KeyEvent event) { // Handle media key here because it is related to the menu. if (isMediaStartKey(keyCode)) { // The media key should not be passed up to the system in any cases. - if (mCurrentDialog != null || mProgramGuide.isActive() + if (mCurrentDialog != null + || mProgramGuide.isActive() || mSideFragmentManager.isActive() || mSearchFragment.isVisible() || mTransitionManager.isKeypadChannelSwitchActive() @@ -1015,6 +1027,10 @@ public class TvOverlayManager { // Do not handle media key when any pop-ups which can handle keys are active. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } + if (mTvView.isScreenBlocked()) { + // Do not handle media key when screen is blocked. + return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; + } TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager(); if (!timeShiftManager.isAvailable()) { return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; @@ -1091,9 +1107,10 @@ public class TvOverlayManager { if (timeShiftManager.isPaused()) { timeShiftManager.play(); } - hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); + hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (mMenu.isActive()) { @@ -1109,8 +1126,8 @@ public class TvOverlayManager { mTransitionManager.goToEmptyScene(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } - return mKeypadChannelSwitchView.onKeyUp(keyCode, event) ? - MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED + return mKeypadChannelSwitchView.onKeyUp(keyCode, event) + ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mTransitionManager.isSelectInputActive()) { @@ -1118,8 +1135,8 @@ public class TvOverlayManager { mTransitionManager.goToEmptyScene(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } - return mSelectInputView.onKeyUp(keyCode, event) ? - MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED + return mSelectInputView.onKeyUp(keyCode, event) + ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mSetupFragmentActive) { @@ -1139,9 +1156,7 @@ public class TvOverlayManager { return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH; } - /** - * Checks whether the given {@code keyCode} can start the system's music app or not. - */ + /** Checks whether the given {@code keyCode} can start the system's music app or not. */ private static boolean isMediaStartKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: @@ -1190,8 +1205,11 @@ public class TvOverlayManager { private final boolean mKeepSidePanelHistory; private final boolean mKeepProgramGuide; - PendingDialogAction(String tag, SafeDismissDialogFragment dialog, - boolean keepSidePanelHistory, boolean keepProgramGuide) { + PendingDialogAction( + String tag, + SafeDismissDialogFragment dialog, + boolean keepSidePanelHistory, + boolean keepProgramGuide) { mTag = tag; mDialog = dialog; mKeepSidePanelHistory = keepSidePanelHistory; diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java index 628bbb72..5af3e6f2 100644 --- a/src/com/android/tv/ui/TvTransitionManager.java +++ b/src/com/android/tv/ui/TvTransitionManager.java @@ -30,19 +30,23 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; - +import com.android.tv.data.api.Channel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public class TvTransitionManager extends TransitionManager { @Retention(RetentionPolicy.SOURCE) - @IntDef({SCENE_TYPE_EMPTY, SCENE_TYPE_CHANNEL_BANNER, SCENE_TYPE_INPUT_BANNER, - SCENE_TYPE_KEYPAD_CHANNEL_SWITCH, SCENE_TYPE_SELECT_INPUT}) + @IntDef({ + SCENE_TYPE_EMPTY, + SCENE_TYPE_CHANNEL_BANNER, + SCENE_TYPE_INPUT_BANNER, + SCENE_TYPE_KEYPAD_CHANNEL_SWITCH, + SCENE_TYPE_SELECT_INPUT + }) public @interface SceneType {} + public static final int SCENE_TYPE_EMPTY = 0; public static final int SCENE_TYPE_CHANNEL_BANNER = 1; public static final int SCENE_TYPE_INPUT_BANNER = 2; @@ -70,17 +74,24 @@ public class TvTransitionManager extends TransitionManager { private Listener mListener; - public TvTransitionManager(MainActivity mainActivity, ViewGroup sceneContainer, - ChannelBannerView channelBannerView, InputBannerView inputBannerView, - KeypadChannelSwitchView keypadChannelSwitchView, SelectInputView selectInputView) { + public TvTransitionManager( + MainActivity mainActivity, + ViewGroup sceneContainer, + ChannelBannerView channelBannerView, + InputBannerView inputBannerView, + KeypadChannelSwitchView keypadChannelSwitchView, + SelectInputView selectInputView) { mMainActivity = mainActivity; mSceneContainer = sceneContainer; mChannelBannerView = channelBannerView; mInputBannerView = inputBannerView; mKeypadChannelSwitchView = keypadChannelSwitchView; mSelectInputView = selectInputView; - mEmptyView = (FrameLayout) mMainActivity.getLayoutInflater().inflate( - R.layout.empty_info_banner, sceneContainer, false); + mEmptyView = + (FrameLayout) + mMainActivity + .getLayoutInflater() + .inflate(R.layout.empty_info_banner, sceneContainer, false); mCurrentSceneView = mEmptyView; } @@ -108,8 +119,10 @@ public class TvTransitionManager extends TransitionManager { if (mCurrentScene != mInputBannerScene) { // Show the input banner instead. LayoutParams lp = (LayoutParams) mInputBannerView.getLayoutParams(); - lp.width = mCurrentScene == mSelectInputScene ? mSelectInputView.getWidth() - : FrameLayout.LayoutParams.WRAP_CONTENT; + lp.width = + mCurrentScene == mSelectInputScene + ? mSelectInputView.getWidth() + : FrameLayout.LayoutParams.WRAP_CONTENT; mInputBannerView.setLayoutParams(lp); mInputBannerView.updateLabel(); transitionTo(mInputBannerScene); @@ -154,33 +167,35 @@ public class TvTransitionManager extends TransitionManager { if (mInitialized) { return; } - mEnterAnimator = AnimatorInflater.loadAnimator(mMainActivity, - R.animator.channel_banner_enter); - mExitAnimator = AnimatorInflater.loadAnimator(mMainActivity, - R.animator.channel_banner_exit); + mEnterAnimator = + AnimatorInflater.loadAnimator(mMainActivity, R.animator.channel_banner_enter); + mExitAnimator = + AnimatorInflater.loadAnimator(mMainActivity, R.animator.channel_banner_exit); mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView); - mEmptyScene.setEnterAction(new Runnable() { - @Override - public void run() { - FrameLayout.LayoutParams emptySceneLayoutParams = - (FrameLayout.LayoutParams) mEmptyView.getLayoutParams(); - ViewGroup.MarginLayoutParams lp = - (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams(); - emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop(); - emptySceneLayoutParams.setMarginStart(lp.getMarginStart()); - emptySceneLayoutParams.height = mCurrentSceneView.getHeight(); - emptySceneLayoutParams.width = mCurrentSceneView.getWidth(); - mEmptyView.setLayoutParams(emptySceneLayoutParams); - setCurrentScene(mEmptyScene, mEmptyView); - } - }); - mEmptyScene.setExitAction(new Runnable() { - @Override - public void run() { - removeAllViewsFromOverlay(); - } - }); + mEmptyScene.setEnterAction( + new Runnable() { + @Override + public void run() { + FrameLayout.LayoutParams emptySceneLayoutParams = + (FrameLayout.LayoutParams) mEmptyView.getLayoutParams(); + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams(); + emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop(); + emptySceneLayoutParams.setMarginStart(lp.getMarginStart()); + emptySceneLayoutParams.height = mCurrentSceneView.getHeight(); + emptySceneLayoutParams.width = mCurrentSceneView.getWidth(); + mEmptyView.setLayoutParams(emptySceneLayoutParams); + setCurrentScene(mEmptyScene, mEmptyView); + } + }); + mEmptyScene.setExitAction( + new Runnable() { + @Override + public void run() { + removeAllViewsFromOverlay(); + } + }); mChannelBannerScene = buildScene(mSceneContainer, mChannelBannerView); mInputBannerScene = buildScene(mSceneContainer, mInputBannerView); @@ -189,18 +204,20 @@ public class TvTransitionManager extends TransitionManager { mCurrentScene = mEmptyScene; // Enter transitions - TransitionSet enter = new TransitionSet() - .addTransition(new SceneTransition(SceneTransition.ENTER)) - .addTransition(new Fade(Fade.IN)); + TransitionSet enter = + new TransitionSet() + .addTransition(new SceneTransition(SceneTransition.ENTER)) + .addTransition(new Fade(Fade.IN)); setTransition(mEmptyScene, mChannelBannerScene, enter); setTransition(mEmptyScene, mInputBannerScene, enter); setTransition(mEmptyScene, mKeypadChannelSwitchScene, enter); setTransition(mEmptyScene, mSelectInputScene, enter); // Exit transitions - TransitionSet exit = new TransitionSet() - .addTransition(new SceneTransition(SceneTransition.EXIT)) - .addTransition(new Fade(Fade.OUT)); + TransitionSet exit = + new TransitionSet() + .addTransition(new SceneTransition(SceneTransition.EXIT)) + .addTransition(new Fade(Fade.OUT)); setTransition(mChannelBannerScene, mEmptyScene, exit); setTransition(mInputBannerScene, mEmptyScene, exit); setTransition(mKeypadChannelSwitchScene, mEmptyScene, exit); @@ -220,10 +237,9 @@ public class TvTransitionManager extends TransitionManager { mInitialized = true; } - /** - * Returns the type of the given scene. - */ - @SceneType public int getSceneType(Scene scene) { + /** Returns the type of the given scene. */ + @SceneType + public int getSceneType(Scene scene) { if (scene == mChannelBannerScene) { return SCENE_TYPE_CHANNEL_BANNER; } else if (scene == mInputBannerScene) { @@ -257,21 +273,23 @@ public class TvTransitionManager extends TransitionManager { private Scene buildScene(ViewGroup sceneRoot, final TransitionLayout layout) { final Scene scene = new Scene(sceneRoot, (View) layout); - scene.setEnterAction(new Runnable() { - @Override - public void run() { - boolean wasEmptyScene = (mCurrentScene == mEmptyScene); - setCurrentScene(scene, (ViewGroup) layout); - layout.onEnterAction(wasEmptyScene); - } - }); - scene.setExitAction(new Runnable() { - @Override - public void run() { - removeAllViewsFromOverlay(); - layout.onExitAction(); - } - }); + scene.setEnterAction( + new Runnable() { + @Override + public void run() { + boolean wasEmptyScene = (mCurrentScene == mEmptyScene); + setCurrentScene(scene, (ViewGroup) layout); + layout.onEnterAction(wasEmptyScene); + } + }); + scene.setExitAction( + new Runnable() { + @Override + public void run() { + removeAllViewsFromOverlay(); + layout.onExitAction(); + } + }); return scene; } @@ -294,12 +312,10 @@ public class TvTransitionManager extends TransitionManager { } @Override - public void captureStartValues(TransitionValues transitionValues) { - } + public void captureStartValues(TransitionValues transitionValues) {} @Override - public void captureEndValues(TransitionValues transitionValues) { - } + public void captureEndValues(TransitionValues transitionValues) {} @Override public Animator createAnimator( @@ -311,9 +327,7 @@ public class TvTransitionManager extends TransitionManager { } } - /** - * An interface for notification of the scene transition. - */ + /** An interface for notification of the scene transition. */ public interface Listener { /** * Called when the scene changes. This method is called just before the scene transition. diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index f042987a..7e354db3 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -42,16 +42,15 @@ import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; - -import com.android.tv.Features; import com.android.tv.R; +import com.android.tv.TvFeatures; import com.android.tv.TvOptionsManager; import com.android.tv.data.DisplayMode; import com.android.tv.util.TvSettings; /** - * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView. - * It also control the settings regarding TvView UI such as display mode. + * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView. It + * also control the settings regarding TvView UI such as display mode. */ public class TvViewUiManager { private static final String TAG = "TvViewManager"; @@ -94,7 +93,7 @@ public class TvViewUiManager { mTvView.setLayoutParams(mTvViewFrame); // Smooth PIP size change, we don't change surface size when // isInPictureInPictureMode is true. - if (!Features.PICTURE_IN_PICTURE.isEnabled(mContext) + if (!TvFeatures.PICTURE_IN_PICTURE.isEnabled(mContext) || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !((Activity) mContext).isInPictureInPictureMode())) { mTvView.setFixedSurfaceSize( @@ -126,16 +125,19 @@ public class TvViewUiManager { private int mAppliedTvViewEndMargin; private float mAppliedVideoDisplayAspectRatio; - public TvViewUiManager(Context context, TunableTvView tvView, - FrameLayout contentView, TvOptionsManager tvOptionManager) { + public TvViewUiManager( + Context context, + TunableTvView tvView, + FrameLayout contentView, + TvOptionsManager tvOptionManager) { mContext = context; mResources = mContext.getResources(); mTvView = tvView; mContentView = contentView; mTvOptionsManager = tvOptionManager; - DisplayManager displayManager = (DisplayManager) mContext - .getSystemService(Context.DISPLAY_SERVICE); + DisplayManager displayManager = + (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); Point size = new Point(); display.getSize(size); @@ -143,8 +145,8 @@ public class TvViewUiManager { mWindowHeight = size.y; // Have an assumption that TvView Shrinking happens only in full screen. - mTvViewShrunkenStartMargin = mResources - .getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); + mTvViewShrunkenStartMargin = + mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start); mTvViewShrunkenEndMargin = mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end) + mResources.getDimensionPixelSize(R.dimen.side_panel_width); @@ -152,10 +154,12 @@ public class TvViewUiManager { mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - mLinearOutSlowIn = AnimationUtils - .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); - mFastOutLinearIn = AnimationUtils - .loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in); + mLinearOutSlowIn = + AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.linear_out_slow_in); + mFastOutLinearIn = + AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_linear_in); } public void onConfigurationChanged(final int windowWidth, final int windowHeight) { @@ -169,9 +173,9 @@ public class TvViewUiManager { } /** - * Initializes animator in advance of using the animator to improve animation performance. - * For fast first tune, it is not expected to be called in Activity.onCreate, but called - * a few seconds later after onCreate. + * Initializes animator in advance of using the animator to improve animation performance. For + * fast first tune, it is not expected to be called in Activity.onCreate, but called a few + * seconds later after onCreate. */ public void initAnimatorIfNeeded() { initTvAnimatorIfNeeded(); @@ -203,16 +207,14 @@ public class TvViewUiManager { setDisplayMode(mDisplayModeBeforeShrunken, false, true); } - /** - * Returns true, if TvView is shrunken. - */ + /** Returns true, if TvView is shrunken. */ public boolean isUnderShrunkenTvView() { return mIsUnderShrunkenTvView; } /** - * Returns true, if {@code displayMode} is available now. If screen ratio is matched to - * video ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available. + * Returns true, if {@code displayMode} is available now. If screen ratio is matched to video + * ratio, other display modes than {@link DisplayMode#MODE_NORMAL} are not available. */ public boolean isDisplayModeAvailable(int displayMode) { if (displayMode == DisplayMode.MODE_NORMAL) { @@ -226,11 +228,15 @@ public class TvViewUiManager { if (viewWidth <= 0 || viewHeight <= 0 || videoDisplayAspectRatio <= 0f) { Log.w(TAG, "Video size is currently unavailable"); if (DEBUG) { - Log.d(TAG, "isDisplayModeAvailable: " - + "viewWidth=" + viewWidth - + ", viewHeight=" + viewHeight - + ", videoDisplayAspectRatio=" + videoDisplayAspectRatio - ); + Log.d( + TAG, + "isDisplayModeAvailable: " + + "viewWidth=" + + viewWidth + + ", viewHeight=" + + viewHeight + + ", videoDisplayAspectRatio=" + + videoDisplayAspectRatio); } return false; } @@ -239,9 +245,7 @@ public class TvViewUiManager { return Math.abs(viewRatio - videoDisplayAspectRatio) >= DISPLAY_MODE_EPSILON; } - /** - * Returns a constant defined in DisplayMode. - */ + /** Returns a constant defined in DisplayMode. */ public int getDisplayMode() { if (isDisplayModeAvailable(mDisplayMode)) { return mDisplayMode; @@ -264,49 +268,45 @@ public class TvViewUiManager { return prev; } - /** - * Restores the display mode to the display mode stored in preference. - */ + /** Restores the display mode to the display mode stored in preference. */ public void restoreDisplayMode(boolean animate) { - int displayMode = mSharedPreferences - .getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL); + int displayMode = + mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE, DisplayMode.MODE_NORMAL); setDisplayMode(displayMode, false, animate); } - /** - * Updates TvView's aspect ratio. It should be called when video resolution is changed. - */ + /** Updates TvView's aspect ratio. It should be called when video resolution is changed. */ public void updateTvAspectRatio() { applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false); if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) { - mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), - mFastOutLinearIn, null); + mTvView.fadeIn( + mResources.getInteger(R.integer.tvview_fade_in_duration), + mFastOutLinearIn, + null); } } - /** - * Fades in TvView. - */ + /** Fades in TvView. */ public void fadeInTvView() { if (mTvView.isFadedOut()) { - mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration), - mFastOutLinearIn, null); + mTvView.fadeIn( + mResources.getInteger(R.integer.tvview_fade_in_duration), + mFastOutLinearIn, + null); } } - /** - * Fades out TvView. - */ + /** Fades out TvView. */ public void fadeOutTvView(Runnable postAction) { if (!mTvView.isFadedOut()) { - mTvView.fadeOut(mResources.getInteger(R.integer.tvview_fade_out_duration), - mLinearOutSlowIn, postAction); + mTvView.fadeOut( + mResources.getInteger(R.integer.tvview_fade_out_duration), + mLinearOutSlowIn, + postAction); } } - /** - * This margins will be applied when applyDisplayMode is called. - */ + /** This margins will be applied when applyDisplayMode is called. */ private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) { mTvViewStartMargin = tvViewStartMargin; mTvViewEndMargin = tvViewEndMargin; @@ -316,8 +316,8 @@ public class TvViewUiManager { return mTvViewStartMargin == 0 && mTvViewEndMargin == 0; } - private void setBackgroundColor(int color, FrameLayout.LayoutParams targetLayoutParams, - boolean animate) { + private void setBackgroundColor( + int color, FrameLayout.LayoutParams targetLayoutParams, boolean animate) { if (animate) { initBackgroundAnimatorIfNeeded(); if (mBackgroundAnimator.isStarted()) { @@ -327,12 +327,13 @@ public class TvViewUiManager { int decorViewWidth = mContentView.getWidth(); int decorViewHeight = mContentView.getHeight(); - boolean hasPillarBox = mTvView.getWidth() != decorViewWidth - || mTvView.getHeight() != decorViewHeight; - boolean willHavePillarBox = ((targetLayoutParams.width != LayoutParams.MATCH_PARENT) - && targetLayoutParams.width != decorViewWidth) || ( - (targetLayoutParams.height != LayoutParams.MATCH_PARENT) - && targetLayoutParams.height != decorViewHeight); + boolean hasPillarBox = + mTvView.getWidth() != decorViewWidth || mTvView.getHeight() != decorViewHeight; + boolean willHavePillarBox = + ((targetLayoutParams.width != LayoutParams.MATCH_PARENT) + && targetLayoutParams.width != decorViewWidth) + || ((targetLayoutParams.height != LayoutParams.MATCH_PARENT) + && targetLayoutParams.height != decorViewHeight); if (!isTvViewFullScreen() && !hasPillarBox) { // If there is no pillar box, no animation is needed. @@ -351,13 +352,27 @@ public class TvViewUiManager { mBackgroundColor = color; } - private void setTvViewPosition(final FrameLayout.LayoutParams layoutParams, - FrameLayout.LayoutParams tvViewFrame, boolean animate) { + private void setTvViewPosition( + final FrameLayout.LayoutParams layoutParams, + FrameLayout.LayoutParams tvViewFrame, + boolean animate) { if (DEBUG) { - Log.d(TAG, "setTvViewPosition: w=" + layoutParams.width + " h=" + layoutParams.height - + " s=" + layoutParams.getMarginStart() + " t=" + layoutParams.topMargin - + " e=" + layoutParams.getMarginEnd() + " b=" + layoutParams.bottomMargin - + " animate=" + animate); + Log.d( + TAG, + "setTvViewPosition: w=" + + layoutParams.width + + " h=" + + layoutParams.height + + " s=" + + layoutParams.getMarginStart() + + " t=" + + layoutParams.topMargin + + " e=" + + layoutParams.getMarginEnd() + + " b=" + + layoutParams.bottomMargin + + " animate=" + + animate); } FrameLayout.LayoutParams oldTvViewFrame = mTvViewFrame; mTvViewLayoutParams = layoutParams; @@ -372,21 +387,25 @@ public class TvViewUiManager { mOldTvViewFrame = new FrameLayout.LayoutParams(oldTvViewFrame); } mTvViewAnimator.setObjectValues(mTvView.getTvViewLayoutParams(), layoutParams); - mTvViewAnimator.setEvaluator(new TypeEvaluator<FrameLayout.LayoutParams>() { - FrameLayout.LayoutParams lp; - @Override - public FrameLayout.LayoutParams evaluate(float fraction, - FrameLayout.LayoutParams startValue, FrameLayout.LayoutParams endValue) { - if (lp == null) { - lp = new FrameLayout.LayoutParams(0, 0); - lp.gravity = startValue.gravity; - } - interpolateMargins(lp, startValue, endValue, fraction); - return lp; - } - }); - mTvViewAnimator - .setInterpolator(isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn); + mTvViewAnimator.setEvaluator( + new TypeEvaluator<FrameLayout.LayoutParams>() { + FrameLayout.LayoutParams lp; + + @Override + public FrameLayout.LayoutParams evaluate( + float fraction, + FrameLayout.LayoutParams startValue, + FrameLayout.LayoutParams endValue) { + if (lp == null) { + lp = new FrameLayout.LayoutParams(0, 0); + lp.gravity = startValue.gravity; + } + interpolateMargins(lp, startValue, endValue, fraction); + return lp; + } + }); + mTvViewAnimator.setInterpolator( + isTvViewFullScreen() ? mFastOutLinearIn : mLinearOutSlowIn); mTvViewAnimator.start(); } else { if (mTvViewAnimator != null && mTvViewAnimator.isStarted()) { @@ -425,38 +444,42 @@ public class TvViewUiManager { mTvViewAnimator.setProperty( Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams")); mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration)); - mTvViewAnimator.addListener(new AnimatorListenerAdapter() { - private boolean mCanceled = false; + mTvViewAnimator.addListener( + new AnimatorListenerAdapter() { + private boolean mCanceled = false; - @Override - public void onAnimationCancel(Animator animation) { - mCanceled = true; - } + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } - @Override - public void onAnimationEnd(Animator animation) { - if (mCanceled) { - mCanceled = false; - return; - } - mHandler.post(new Runnable() { @Override - public void run() { - setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false); + public void onAnimationEnd(Animator animation) { + if (mCanceled) { + mCanceled = false; + return; + } + mHandler.post( + new Runnable() { + @Override + public void run() { + setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false); + } + }); + } + }); + mTvViewAnimator.addUpdateListener( + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + float fraction = animator.getAnimatedFraction(); + mLastAnimatedTvViewFrame = + (FrameLayout.LayoutParams) mTvView.getLayoutParams(); + interpolateMargins( + mLastAnimatedTvViewFrame, mOldTvViewFrame, mTvViewFrame, fraction); + mTvView.setLayoutParams(mLastAnimatedTvViewFrame); } }); - } - }); - mTvViewAnimator.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - float fraction = animator.getAnimatedFraction(); - mLastAnimatedTvViewFrame = (FrameLayout.LayoutParams) mTvView.getLayoutParams(); - interpolateMargins(mLastAnimatedTvViewFrame, - mOldTvViewFrame, mTvViewFrame, fraction); - mTvView.setLayoutParams(mLastAnimatedTvViewFrame); - } - }); } private void initBackgroundAnimatorIfNeeded() { @@ -467,31 +490,33 @@ public class TvViewUiManager { mBackgroundAnimator = new ObjectAnimator(); mBackgroundAnimator.setTarget(mContentView); mBackgroundAnimator.setPropertyName("backgroundColor"); - mBackgroundAnimator - .setDuration(mResources.getInteger(R.integer.tvactivity_background_anim_duration)); - mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mHandler.post(new Runnable() { + mBackgroundAnimator.setDuration( + mResources.getInteger(R.integer.tvactivity_background_anim_duration)); + mBackgroundAnimator.addListener( + new AnimatorListenerAdapter() { @Override - public void run() { - mContentView.setBackgroundColor(mBackgroundColor); + public void onAnimationEnd(Animator animation) { + mHandler.post( + new Runnable() { + @Override + public void run() { + mContentView.setBackgroundColor(mBackgroundColor); + } + }); } }); - } - }); } - private void applyDisplayMode(float videoDisplayAspectRatio, boolean animate, - boolean forceUpdate) { + private void applyDisplayMode( + float videoDisplayAspectRatio, boolean animate, boolean forceUpdate) { if (videoDisplayAspectRatio <= 0f) { videoDisplayAspectRatio = (float) mWindowWidth / mWindowHeight; } if (mAppliedDisplayedMode == mDisplayMode && mAppliedTvViewStartMargin == mTvViewStartMargin && mAppliedTvViewEndMargin == mTvViewEndMargin - && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) < - DISPLAY_ASPECT_RATIO_EPSILON) { + && Math.abs(mAppliedVideoDisplayAspectRatio - videoDisplayAspectRatio) + < DISPLAY_ASPECT_RATIO_EPSILON) { if (!forceUpdate) { return; } @@ -507,14 +532,20 @@ public class TvViewUiManager { float availableAreaRatio = 0; if (availableAreaWidth <= 0 || availableAreaHeight <= 0) { displayMode = DisplayMode.MODE_FULL; - Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" - + "availableAreaWidth=" + availableAreaWidth + ", availableAreaHeight=" - + availableAreaHeight + ")"); + Log.w( + TAG, + "Some resolution info is missing during applyDisplayMode. (" + + "availableAreaWidth=" + + availableAreaWidth + + ", availableAreaHeight=" + + availableAreaHeight + + ")"); } else { availableAreaRatio = (float) availableAreaWidth / availableAreaHeight; } - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0, - ((FrameLayout.LayoutParams) mTvView.getTvViewLayoutParams()).gravity); + FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams( + 0, 0, ((FrameLayout.LayoutParams) mTvView.getTvViewLayoutParams()).gravity); switch (displayMode) { case DisplayMode.MODE_ZOOM: if (videoDisplayAspectRatio < availableAreaRatio) { @@ -549,12 +580,18 @@ public class TvViewUiManager { int marginStart = (availableAreaWidth - layoutParams.width) / 2; layoutParams.setMarginStart(marginStart); int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2; - FrameLayout.LayoutParams tvViewFrame = createMarginLayoutParams( - mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop); + FrameLayout.LayoutParams tvViewFrame = + createMarginLayoutParams( + mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop); setTvViewPosition(layoutParams, tvViewFrame, animate); - setBackgroundColor(mResources.getColor(isTvViewFullScreen() - ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview, - null), layoutParams, animate); + setBackgroundColor( + mResources.getColor( + isTvViewFullScreen() + ? R.color.tvactivity_background + : R.color.tvactivity_background_on_shrunken_tvview, + null), + layoutParams, + animate); // Update the current display mode. mTvOptionsManager.onDisplayModeChanged(displayMode); @@ -564,12 +601,15 @@ public class TvViewUiManager { return (int) (start + (end - start) * fraction); } - private static void interpolateMargins(MarginLayoutParams out, - MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction) { + private static void interpolateMargins( + MarginLayoutParams out, + MarginLayoutParams startValue, + MarginLayoutParams endValue, + float fraction) { out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction); out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction); - out.setMarginStart(interpolate(startValue.getMarginStart(), endValue.getMarginStart(), - fraction)); + out.setMarginStart( + interpolate(startValue.getMarginStart(), endValue.getMarginStart(), fraction)); out.setMarginEnd(interpolate(startValue.getMarginEnd(), endValue.getMarginEnd(), fraction)); out.width = interpolate(startValue.width, endValue.width, fraction); out.height = interpolate(startValue.height, endValue.height, fraction); @@ -586,4 +626,4 @@ public class TvViewUiManager { lp.height = mWindowHeight - topMargin - bottomMargin; return lp; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/ViewUtils.java b/src/com/android/tv/ui/ViewUtils.java index ac181752..f64a70b2 100644 --- a/src/com/android/tv/ui/ViewUtils.java +++ b/src/com/android/tv/ui/ViewUtils.java @@ -21,13 +21,10 @@ import android.animation.ValueAnimator; import android.util.Log; import android.view.View; import android.view.ViewGroup.LayoutParams; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -/** - * A class that includes convenience methods for view classes. - */ +/** A class that includes convenience methods for view classes. */ public class ViewUtils { private static final String TAG = "ViewUtils"; @@ -40,49 +37,49 @@ public class ViewUtils { try { method = View.class.getDeclaredMethod("setTransitionAlpha", Float.TYPE); method.invoke(v, alpha); - } catch (NoSuchMethodException|IllegalAccessException|IllegalArgumentException - |InvocationTargetException e) { + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { Log.e(TAG, "Fail to call View.setTransitionAlpha", e); } } /** * Creates an animator in view's height + * * @param target the {@link view} animator performs on. */ public static Animator createHeightAnimator( final View target, int initialHeight, int targetHeight) { ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - if (value == 0) { - if (target.getVisibility() != View.GONE) { - target.setVisibility(View.GONE); + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int value = (Integer) animation.getAnimatedValue(); + if (value == 0) { + if (target.getVisibility() != View.GONE) { + target.setVisibility(View.GONE); + } + } else { + if (target.getVisibility() != View.VISIBLE) { + target.setVisibility(View.VISIBLE); + } + setLayoutHeight(target, value); + } } - } else { - if (target.getVisibility() != View.VISIBLE) { - target.setVisibility(View.VISIBLE); - } - setLayoutHeight(target, value); - } - } - }); + }); return animator; } - /** - * Gets view's layout height. - */ + /** Gets view's layout height. */ public static int getLayoutHeight(View view) { LayoutParams layoutParams = view.getLayoutParams(); return layoutParams.height; } - /** - * Sets view's layout height. - */ + /** Sets view's layout height. */ public static void setLayoutHeight(View view, int height) { LayoutParams layoutParams = view.getLayoutParams(); if (height != layoutParams.height) { @@ -90,4 +87,4 @@ public class ViewUtils { view.setLayoutParams(layoutParams); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java new file mode 100644 index 00000000..75859792 --- /dev/null +++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java @@ -0,0 +1,98 @@ +package com.android.tv.ui.hideable; + +import android.content.Context; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.support.annotation.VisibleForTesting; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import com.android.tv.common.WeakHandler; + +/** + * Schedules a view element to be hidden after a delay. + * + * <p>When accessibility is turned on elements are not automatically hidden. + * + * <p>Users of this class must pass it to {@link + * AccessibilityManager#addAccessibilityStateChangeListener(AccessibilityStateChangeListener)} and + * {@link + * AccessibilityManager#removeAccessibilityStateChangeListener(AccessibilityStateChangeListener)} + * during the appropriate live cycle event, or handle calling {@link + * #onAccessibilityStateChanged(boolean)}. + */ +@UiThread +public final class AutoHideScheduler implements AccessibilityStateChangeListener { + private static final int MSG_HIDE = 1; + + private final HideHandler mHandler; + private final Runnable mRunnable; + + public AutoHideScheduler(Context context, Runnable runnable) { + this( + runnable, + context.getSystemService(AccessibilityManager.class), + Looper.getMainLooper()); + } + + @VisibleForTesting + AutoHideScheduler(Runnable runnable, AccessibilityManager accessibilityManager, Looper looper) { + // Keep a reference here because HideHandler only has a weak reference to it. + mRunnable = runnable; + mHandler = new HideHandler(looper, mRunnable); + mHandler.setAllowAutoHide(!accessibilityManager.isEnabled()); + } + + public void cancel() { + mHandler.removeMessages(MSG_HIDE); + } + + public void schedule(long delayMs) { + cancel(); + if (mHandler.mAllowAutoHide) { + mHandler.sendEmptyMessageDelayed(MSG_HIDE, delayMs); + } + } + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mHandler.onAccessibilityStateChanged(enabled); + } + + public boolean isScheduled() { + return mHandler.hasMessages(MSG_HIDE); + } + + private static class HideHandler extends WeakHandler<Runnable> + implements AccessibilityStateChangeListener { + + private boolean mAllowAutoHide; + + public HideHandler(Looper looper, Runnable hideRunner) { + super(looper, hideRunner); + } + + @Override + protected void handleMessage(Message msg, @NonNull Runnable runnable) { + switch (msg.what) { + case MSG_HIDE: + if (mAllowAutoHide) { + runnable.run(); + } + break; + default: + // do nothing + } + } + + public void setAllowAutoHide(boolean mAllowAutoHide) { + this.mAllowAutoHide = mAllowAutoHide; + } + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mAllowAutoHide = !enabled; + } + } +} diff --git a/src/com/android/tv/ui/sidepanel/ActionItem.java b/src/com/android/tv/ui/sidepanel/ActionItem.java index cd70a886..73c12080 100644 --- a/src/com/android/tv/ui/sidepanel/ActionItem.java +++ b/src/com/android/tv/ui/sidepanel/ActionItem.java @@ -18,7 +18,6 @@ package com.android.tv.ui.sidepanel; import android.view.View; import android.widget.TextView; - import com.android.tv.R; public abstract class ActionItem extends Item { @@ -52,4 +51,4 @@ public abstract class ActionItem extends Item { descriptionView.setVisibility(View.GONE); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java b/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java index 8389675e..2726839c 100644 --- a/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java +++ b/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java @@ -19,14 +19,13 @@ package com.android.tv.ui.sidepanel; import android.text.TextUtils; import android.view.View; import android.widget.TextView; - import com.android.tv.R; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.ChannelListener; import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.data.api.Channel; public abstract class ChannelCheckItem extends CompoundButtonItem { private final ChannelDataManager mChannelDataManager; @@ -34,25 +33,27 @@ public abstract class ChannelCheckItem extends CompoundButtonItem { private Channel mChannel; private TextView mProgramTitleView; private TextView mChannelNumberView; - private final ChannelListener mChannelListener = new ChannelListener() { - @Override - public void onChannelRemoved(Channel channel) { } - - @Override - public void onChannelUpdated(Channel channel) { - mChannel = channel; - } - }; - - private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener - = new OnCurrentProgramUpdatedListener() { - @Override - public void onCurrentProgramUpdated(long channelId, Program program) { - updateProgramTitle(program); - } - }; - - public ChannelCheckItem(Channel channel, + private final ChannelListener mChannelListener = + new ChannelListener() { + @Override + public void onChannelRemoved(Channel channel) {} + + @Override + public void onChannelUpdated(Channel channel) { + mChannel = channel; + } + }; + + private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener = + new OnCurrentProgramUpdatedListener() { + @Override + public void onCurrentProgramUpdated(long channelId, Program program) { + updateProgramTitle(program); + } + }; + + public ChannelCheckItem( + Channel channel, ChannelDataManager channelDataManager, ProgramDataManager programDataManager) { super(channel.getDisplayName(), ""); @@ -91,8 +92,8 @@ public abstract class ChannelCheckItem extends CompoundButtonItem { mChannelNumberView = (TextView) view.findViewById(R.id.channel_number); mProgramTitleView = (TextView) view.findViewById(R.id.program_title); mChannelDataManager.addChannelListener(mChannel.getId(), mChannelListener); - mProgramDataManager.addOnCurrentProgramUpdatedListener(mChannel.getId(), - mOnCurrentProgramUpdatedListener); + mProgramDataManager.addOnCurrentProgramUpdatedListener( + mChannel.getId(), mOnCurrentProgramUpdatedListener); } @Override @@ -105,8 +106,8 @@ public abstract class ChannelCheckItem extends CompoundButtonItem { @Override protected void onUnbind() { mChannelDataManager.removeChannelListener(mChannel.getId(), mChannelListener); - mProgramDataManager.removeOnCurrentProgramUpdatedListener(mChannel.getId(), - mOnCurrentProgramUpdatedListener); + mProgramDataManager.removeOnCurrentProgramUpdatedListener( + mChannel.getId(), mOnCurrentProgramUpdatedListener); mProgramTitleView = null; mChannelNumberView = null; super.onUnbind(); diff --git a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java index 79c2b0a7..ef828945 100644 --- a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java +++ b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java @@ -22,7 +22,6 @@ import android.view.View; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.TextView; - import com.android.tv.R; public class CheckBoxItem extends CompoundButtonItem { @@ -46,16 +45,17 @@ public class CheckBoxItem extends CompoundButtonItem { super.onBind(view); if (mLayoutForLargeDescription) { CompoundButton checkBox = (CompoundButton) view.findViewById(getCompoundButtonId()); - LinearLayout.LayoutParams lp = - (LinearLayout.LayoutParams) checkBox.getLayoutParams(); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) checkBox.getLayoutParams(); lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - lp.topMargin = view.getResources().getDimensionPixelOffset( - R.dimen.option_item_check_box_margin_top); + lp.topMargin = + view.getResources() + .getDimensionPixelOffset(R.dimen.option_item_check_box_margin_top); checkBox.setLayoutParams(lp); TypedValue outValue = new TypedValue(); - view.getResources().getValue(R.dimen.option_item_check_box_line_spacing_multiplier, - outValue, true); + view.getResources() + .getValue( + R.dimen.option_item_check_box_line_spacing_multiplier, outValue, true); TextView descriptionTextView = (TextView) view.findViewById(getDescriptionViewId()); descriptionTextView.setMaxLines(Integer.MAX_VALUE); @@ -77,4 +77,4 @@ public class CheckBoxItem extends CompoundButtonItem { protected void onSelected() { setChecked(!isChecked()); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java index 341e4350..c1e1d18a 100644 --- a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java @@ -23,16 +23,14 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.android.tv.R; import com.android.tv.util.CaptionSettings; - import java.util.ArrayList; import java.util.List; import java.util.Locale; public class ClosedCaptionFragment extends SideFragment { - private static final String TRACKER_LABEL ="closed caption" ; + private static final String TRACKER_LABEL = "closed caption"; private boolean mResetClosedCaption; private int mClosedCaptionOption; private String mClosedCaptionLanguage; @@ -66,8 +64,10 @@ public class ClosedCaptionFragment extends SideFragment { List<TvTrackInfo> tracks = getMainActivity().getTracks(TvTrackInfo.TYPE_SUBTITLE); if (tracks != null && !tracks.isEmpty()) { - String selectedTrackId = captionSettings.isEnabled() ? - getMainActivity().getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE) : null; + String selectedTrackId = + captionSettings.isEnabled() + ? getMainActivity().getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE) + : null; ClosedCaptionOptionItem item = new ClosedCaptionOptionItem(null, null); items.add(item); if (selectedTrackId == null) { @@ -86,37 +86,41 @@ public class ClosedCaptionFragment extends SideFragment { } } if (getMainActivity().hasCaptioningSettingsActivity()) { - items.add(new ActionItem(getString(R.string.closed_caption_system_settings), - getString(R.string.closed_caption_system_settings_description)) { - @Override - protected void onSelected() { - getMainActivity().startSystemCaptioningSettingsActivity(); - } - - @Override - protected void onFocused() { - super.onFocused(); - if (mSelectedItem != null) { - getMainActivity().selectSubtitleTrack( - mSelectedItem.mOption, mSelectedItem.mTrackId); - } - } - }); + items.add( + new ActionItem( + getString(R.string.closed_caption_system_settings), + getString(R.string.closed_caption_system_settings_description)) { + @Override + protected void onSelected() { + getMainActivity().startSystemCaptioningSettingsActivity(); + } + + @Override + protected void onFocused() { + super.onFocused(); + if (mSelectedItem != null) { + getMainActivity() + .selectSubtitleTrack( + mSelectedItem.mOption, mSelectedItem.mTrackId); + } + } + }); } return items; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onDestroyView() { if (mResetClosedCaption) { - getMainActivity().selectSubtitleLanguage(mClosedCaptionOption, mClosedCaptionLanguage, - mClosedCaptionTrackId); + getMainActivity() + .selectSubtitleLanguage( + mClosedCaptionOption, mClosedCaptionLanguage, mClosedCaptionTrackId); } super.onDestroyView(); } diff --git a/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java b/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java index c2746937..cc09affb 100644 --- a/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java +++ b/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java @@ -19,7 +19,6 @@ package com.android.tv.ui.sidepanel; import android.view.View; import android.widget.CompoundButton; import android.widget.TextView; - import com.android.tv.R; public abstract class CompoundButtonItem extends Item { @@ -44,8 +43,8 @@ public abstract class CompoundButtonItem extends Item { mMaxLine = 0; } - public CompoundButtonItem(String checkedTitle, String uncheckedTitle, String description, - int maxLine) { + public CompoundButtonItem( + String checkedTitle, String uncheckedTitle, String description, int maxLine) { mCheckedTitle = checkedTitle; mUncheckedTitle = uncheckedTitle; mDescription = description; @@ -73,8 +72,10 @@ public abstract class CompoundButtonItem extends Item { descriptionView.setMaxLines(mMaxLine); } else { if (sDefaultMaxLine == 0) { - sDefaultMaxLine = view.getContext().getResources() - .getInteger(R.integer.option_item_description_max_lines); + sDefaultMaxLine = + view.getContext() + .getResources() + .getInteger(R.integer.option_item_description_max_lines); } descriptionView.setMaxLines(sDefaultMaxLine); } diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java index 297e69d9..48b80723 100644 --- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java +++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java @@ -26,16 +26,15 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.data.Channel; +import com.android.tv.common.util.SharedPreferencesUtils; +import com.android.tv.data.ChannelImpl; import com.android.tv.data.ChannelNumber; +import com.android.tv.data.api.Channel; import com.android.tv.ui.OnRepeatedKeyInterceptListener; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -55,7 +54,7 @@ public class CustomizeChannelListFragment extends SideFragment { private static Integer sGroupingType; private TvInputManagerHelper mInputManager; - private Channel.DefaultComparator mChannelComparator; + private ChannelImpl.DefaultComparator mChannelComparator; private boolean mGroupByFragmentRunning; private final List<Item> mItems = new ArrayList<>(); @@ -65,38 +64,46 @@ public class CustomizeChannelListFragment extends SideFragment { super.onCreate(savedInstanceState); mInputManager = getMainActivity().getTvInputManagerHelper(); mInitialChannelId = getMainActivity().getCurrentChannelId(); - mChannelComparator = new Channel.DefaultComparator(getActivity(), mInputManager); + mChannelComparator = new ChannelImpl.DefaultComparator(getActivity(), mInputManager); if (sGroupingType == null) { - SharedPreferences sharedPreferences = getContext().getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE); + SharedPreferences sharedPreferences = + getContext() + .getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, + Context.MODE_PRIVATE); sGroupingType = sharedPreferences.getInt(PREF_KEY_GROUP_SETTINGS, GROUP_BY_SOURCE); } } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); VerticalGridView listView = (VerticalGridView) view.findViewById(R.id.side_panel_list); - listView.setOnKeyInterceptListener(new OnRepeatedKeyInterceptListener(listView) { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - // In order to send tune operation once for continuous channel up/down events, - // we only call the moveToChannel method on ACTION_UP event of channel switch keys. - if (event.getAction() == KeyEvent.ACTION_UP) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - if (mLastFocusedChannelId != Channel.INVALID_ID) { - getMainActivity().tuneToChannel( - getChannelDataManager().getChannel(mLastFocusedChannelId)); + listView.setOnKeyInterceptListener( + new OnRepeatedKeyInterceptListener(listView) { + @Override + public boolean onInterceptKeyEvent(KeyEvent event) { + // In order to send tune operation once for continuous channel up/down + // events, + // we only call the moveToChannel method on ACTION_UP event of channel + // switch keys. + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mLastFocusedChannelId != Channel.INVALID_ID) { + getMainActivity() + .tuneToChannel( + getChannelDataManager() + .getChannel(mLastFocusedChannelId)); + } + break; } - break; + } + return super.onInterceptKeyEvent(event); } - } - return super.onInterceptKeyEvent(event); - } - }); + }); if (!mGroupByFragmentRunning) { getMainActivity().startShrunkenTvView(false, true); @@ -118,8 +125,8 @@ public class CustomizeChannelListFragment extends SideFragment { } mLastFocusedChannelId = mInitialChannelId; MainActivity tvActivity = getMainActivity(); - if (mLastFocusedChannelId != Channel.INVALID_ID && - mLastFocusedChannelId != tvActivity.getCurrentChannelId()) { + if (mLastFocusedChannelId != Channel.INVALID_ID + && mLastFocusedChannelId != tvActivity.getCurrentChannelId()) { tvActivity.tuneToChannel(getChannelDataManager().getChannel(mLastFocusedChannelId)); } } @@ -184,11 +191,11 @@ public class CustomizeChannelListFragment extends SideFragment { Collections.sort(channels, mChannelComparator); String inputId = null; - for (Channel channel: channels) { + for (Channel channel : channels) { if (!channel.getInputId().equals(inputId)) { inputId = channel.getInputId(); - String inputLabel = Utils.loadLabel(getActivity(), - mInputManager.getTvInputInfo(inputId)); + String inputLabel = + Utils.loadLabel(getActivity(), mInputManager.getTvInputInfo(inputId)); items.add(new DividerItem(inputLabel)); selectGroupItem = new SelectGroupItem(); items.add(selectGroupItem); @@ -204,27 +211,32 @@ public class CustomizeChannelListFragment extends SideFragment { items.add(new GroupBySubMenu(getString(R.string.edit_channels_group_by_hd_sd))); SelectGroupItem selectGroupItem = null; ArrayList<Channel> channels = new ArrayList<>(mChannels); - Collections.sort(channels, new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - boolean lhsHd = isHdChannel(lhs); - boolean rhsHd = isHdChannel(rhs); - if (lhsHd == rhsHd) { - return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } else { - return lhsHd ? -1 : 1; - } - } - }); + Collections.sort( + channels, + new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + boolean lhsHd = isHdChannel(lhs); + boolean rhsHd = isHdChannel(rhs); + if (lhsHd == rhsHd) { + return ChannelNumber.compare( + lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } else { + return lhsHd ? -1 : 1; + } + } + }); Boolean isHdGroup = null; - for (Channel channel: channels) { + for (Channel channel : channels) { boolean isHd = isHdChannel(channel); if (isHdGroup == null || isHd != isHdGroup) { isHdGroup = isHd; - items.add(new DividerItem(isHd - ? getString(R.string.edit_channels_group_divider_for_hd) - : getString(R.string.edit_channels_group_divider_for_sd))); + items.add( + new DividerItem( + isHd + ? getString(R.string.edit_channels_group_divider_for_hd) + : getString(R.string.edit_channels_group_divider_for_sd))); selectGroupItem = new SelectGroupItem(); items.add(selectGroupItem); } @@ -237,8 +249,8 @@ public class CustomizeChannelListFragment extends SideFragment { private static boolean isHdChannel(Channel channel) { String videoFormat = channel.getVideoFormat(); - return videoFormat != null && - (Channels.VIDEO_FORMAT_720P.equals(videoFormat) + return videoFormat != null + && (Channels.VIDEO_FORMAT_720P.equals(videoFormat) || Channels.VIDEO_FORMAT_1080I.equals(videoFormat) || Channels.VIDEO_FORMAT_1080P.equals(videoFormat) || Channels.VIDEO_FORMAT_2160P.equals(videoFormat) @@ -274,9 +286,11 @@ public class CustomizeChannelListFragment extends SideFragment { break; } } - mTextView.setText(getString(mAllChecked - ? R.string.edit_channels_item_deselect_group - : R.string.edit_channels_item_select_group)); + mTextView.setText( + getString( + mAllChecked + ? R.string.edit_channels_item_deselect_group + : R.string.edit_channels_item_select_group)); } @Override @@ -290,9 +304,11 @@ public class CustomizeChannelListFragment extends SideFragment { } getChannelDataManager().notifyChannelBrowsableChanged(); mAllChecked = !mAllChecked; - mTextView.setText(getString(mAllChecked - ? R.string.edit_channels_item_deselect_group - : R.string.edit_channels_item_select_group)); + mTextView.setText( + getString( + mAllChecked + ? R.string.edit_channels_item_deselect_group + : R.string.edit_channels_item_select_group)); } } @@ -331,6 +347,7 @@ public class CustomizeChannelListFragment extends SideFragment { protected String getTitle() { return getString(R.string.side_panel_title_group_by); } + @Override public String getTrackerLabel() { return GroupBySubMenu.TRACKER_LABEL; @@ -339,40 +356,46 @@ public class CustomizeChannelListFragment extends SideFragment { @Override protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); - items.add(new RadioButtonItem( - getString(R.string.edit_channels_group_by_sources)) { - @Override - protected void onSelected() { - super.onSelected(); - setGroupingType(GROUP_BY_SOURCE); - closeFragment(); - } - }); - items.add(new RadioButtonItem( - getString(R.string.edit_channels_group_by_hd_sd)) { - @Override - protected void onSelected() { - super.onSelected(); - setGroupingType(GROUP_BY_HD_SD); - closeFragment(); - } - }); + items.add( + new RadioButtonItem(getString(R.string.edit_channels_group_by_sources)) { + @Override + protected void onSelected() { + super.onSelected(); + setGroupingType(GROUP_BY_SOURCE); + closeFragment(); + } + }); + items.add( + new RadioButtonItem(getString(R.string.edit_channels_group_by_hd_sd)) { + @Override + protected void onSelected() { + super.onSelected(); + setGroupingType(GROUP_BY_HD_SD); + closeFragment(); + } + }); ((RadioButtonItem) items.get(sGroupingType)).setChecked(true); return items; } private void setGroupingType(int groupingType) { sGroupingType = groupingType; - SharedPreferences sharedPreferences = getContext().getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE); + SharedPreferences sharedPreferences = + getContext() + .getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, + Context.MODE_PRIVATE); sharedPreferences.edit().putInt(PREF_KEY_GROUP_SETTINGS, groupingType).apply(); } } private class GroupBySubMenu extends SubMenuItem { private static final String TRACKER_LABEL = "Group by"; + public GroupBySubMenu(String description) { - super(getString(R.string.edit_channels_item_group_by), description, + super( + getString(R.string.edit_channels_item_group_by), + description, getMainActivity().getOverlayManager().getSideFragmentManager()); } diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java index f633fa5a..36ee5a2d 100644 --- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java @@ -21,22 +21,20 @@ import android.app.Activity; import android.support.annotation.NonNull; import android.util.Log; import android.widget.Toast; - import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.BuildConfig; +import com.android.tv.TvSingletons; +import com.android.tv.common.CommonPreferences; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.experiments.Experiments; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.util.Utils; +import com.android.tv.common.util.CommonUtils; + + + + import java.util.ArrayList; import java.util.List; -/** - * Options for developers only - */ +/** Options for developers only */ public class DeveloperOptionFragment extends SideFragment { private static final String TAG = "DeveloperOptionFragment"; private static final String TRACKER_LABEL = "debug options"; @@ -55,42 +53,46 @@ public class DeveloperOptionFragment extends SideFragment { protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); if (CommonFeatures.DVR.isEnabled(getContext())) { - items.add(new ActionItem(getString(R.string.dev_item_dvr_history)) { - @Override - protected void onSelected() { - getMainActivity().getOverlayManager().showDvrHistoryDialog(); - } - }); + items.add( + new ActionItem(getString(R.string.dev_item_dvr_history)) { + @Override + protected void onSelected() { + getMainActivity().getOverlayManager().showDvrHistoryDialog(); + } + }); } - if (Utils.isDeveloper()) { - items.add(new ActionItem(getString(R.string.dev_item_watch_history)) { - @Override - protected void onSelected() { - getMainActivity().getOverlayManager().showRecentlyWatchedDialog(); - } - }); + if (CommonUtils.isDeveloper()) { + items.add( + new ActionItem(getString(R.string.dev_item_watch_history)) { + @Override + protected void onSelected() { + getMainActivity().getOverlayManager().showRecentlyWatchedDialog(); + } + }); } - items.add(new SwitchItem(getString(R.string.dev_item_store_ts_on), - getString(R.string.dev_item_store_ts_off), - getString(R.string.dev_item_store_ts_description)) { - @Override - protected void onUpdate() { - super.onUpdate(); - setChecked(TunerPreferences.getStoreTsStream(getContext())); - } + items.add( + new SwitchItem( + getString(R.string.dev_item_store_ts_on), + getString(R.string.dev_item_store_ts_off), + getString(R.string.dev_item_store_ts_description)) { + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked(CommonPreferences.getStoreTsStream(getContext())); + } - @Override - protected void onSelected() { - super.onSelected(); - TunerPreferences.setStoreTsStream(getContext(), isChecked()); - } - }); - if (Utils.isDeveloper()) { + @Override + protected void onSelected() { + super.onSelected(); + CommonPreferences.setStoreTsStream(getContext(), isChecked()); + } + }); + if (CommonUtils.isDeveloper()) { items.add( new ActionItem(getString(R.string.dev_item_show_performance_monitor_log)) { @Override protected void onSelected() { - TvApplication.getSingletons(getContext()) + TvSingletons.getSingletons(getContext()) .getPerformanceMonitor() .startPerformanceMonitorEventDebugActivity(getContext()); } @@ -98,5 +100,4 @@ public class DeveloperOptionFragment extends SideFragment { } return items; } - -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/DisplayModeFragment.java b/src/com/android/tv/ui/sidepanel/DisplayModeFragment.java index 29792757..c51fb38e 100644 --- a/src/com/android/tv/ui/sidepanel/DisplayModeFragment.java +++ b/src/com/android/tv/ui/sidepanel/DisplayModeFragment.java @@ -17,11 +17,9 @@ package com.android.tv.ui.sidepanel; import android.content.Context; - import com.android.tv.R; import com.android.tv.data.DisplayMode; import com.android.tv.ui.TvViewUiManager; - import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/ui/sidepanel/DividerItem.java b/src/com/android/tv/ui/sidepanel/DividerItem.java index 0edf8488..3c237ffc 100644 --- a/src/com/android/tv/ui/sidepanel/DividerItem.java +++ b/src/com/android/tv/ui/sidepanel/DividerItem.java @@ -18,14 +18,13 @@ package com.android.tv.ui.sidepanel; import android.view.View; import android.widget.TextView; - import com.android.tv.R; public class DividerItem extends Item { private TextView mTitleView; private String mTitle; - public DividerItem() { } + public DividerItem() {} public DividerItem(String title) { mTitle = title; @@ -46,8 +45,10 @@ public class DividerItem extends Item { } else { mTitleView.setVisibility(View.VISIBLE); mTitleView.setText(mTitle); - view.setMinimumHeight(view.getContext().getResources().getDimensionPixelOffset( - R.dimen.option_item_height)); + view.setMinimumHeight( + view.getContext() + .getResources() + .getDimensionPixelOffset(R.dimen.option_item_height)); } } @@ -57,5 +58,5 @@ public class DividerItem extends Item { } @Override - protected void onSelected() { } + protected void onSelected() {} } diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java index 4e47e75b..693b7536 100644 --- a/src/com/android/tv/ui/sidepanel/Item.java +++ b/src/com/android/tv/ui/sidepanel/Item.java @@ -35,9 +35,7 @@ public abstract class Item { } } - /** - * Sets the item to be clickable or not. - */ + /** Sets the item to be clickable or not. */ public void setClickable(boolean clickable) { mClickable = clickable; if (mItemView != null) { @@ -45,9 +43,7 @@ public abstract class Item { } } - /** - * Returns whether this item is enabled. - */ + /** Returns whether this item is enabled. */ public boolean isEnabled() { return mEnabled; } @@ -69,9 +65,9 @@ public abstract class Item { } /** - * Called after onBind is called and when {@link #notifyUpdated} is called. - * {@link #notifyUpdated} is usually called by {@link SideFragment#notifyItemChanged} and - * {@link SideFragment#notifyItemsChanged}. + * Called after onBind is called and when {@link #notifyUpdated} is called. {@link + * #notifyUpdated} is usually called by {@link SideFragment#notifyItemChanged} and {@link + * SideFragment#notifyItemsChanged}. */ protected void onUpdate() { setEnabledInternal(mItemView, mEnabled); @@ -80,12 +76,9 @@ public abstract class Item { protected abstract void onSelected(); - protected void onFocused() { - } + protected void onFocused() {} - /** - * Returns true if the item is bound, i.e., onBind is called. - */ + /** Returns true if the item is bound, i.e., onBind is called. */ protected boolean isBound() { return mItemView != null; } @@ -100,4 +93,4 @@ public abstract class Item { } } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java index 10c8c3e2..03b71c8c 100644 --- a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java +++ b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java @@ -19,10 +19,8 @@ package com.android.tv.ui.sidepanel; import android.media.tv.TvTrackInfo; import android.text.TextUtils; import android.view.KeyEvent; - import com.android.tv.R; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.List; @@ -56,9 +54,11 @@ public class MultiAudioFragment extends SideFragment { boolean needToShowSampleRate = Utils.needToShowSampleRate(getActivity(), tracks); int pos = 0; for (final TvTrackInfo track : tracks) { - RadioButtonItem item = new MultiAudioOptionItem( - Utils.getMultiAudioString(getActivity(), track, needToShowSampleRate), - track.getId()); + RadioButtonItem item = + new MultiAudioOptionItem( + Utils.getMultiAudioString( + getActivity(), track, needToShowSampleRate), + track.getId()); if (track.getId().equals(mSelectedTrackId)) { item.setChecked(true); mInitialSelectedPosition = pos; diff --git a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java index e0477493..b6c36795 100644 --- a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java +++ b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java @@ -41,4 +41,4 @@ public class RadioButtonItem extends CompoundButtonItem { protected void onSelected() { setChecked(true); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index 6a5b510c..31d00fa6 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -16,32 +16,28 @@ package com.android.tv.ui.sidepanel; -import static com.android.tv.Features.TUNER; +import static com.android.tv.TvFeatures.TUNER; import android.app.ApplicationErrorReport; import android.content.Intent; import android.media.tv.TvInputInfo; import android.view.View; import android.widget.Toast; - import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.customization.TvCustomizationManager; +import com.android.tv.TvSingletons; +import com.android.tv.common.CommonPreferences; +import com.android.tv.common.customization.CustomizationManager; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.license.LicenseSideFragment; import com.android.tv.license.Licenses; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.util.PermissionUtils; -import com.android.tv.util.SetupUtils; import com.android.tv.util.Utils; - import java.util.ArrayList; import java.util.List; -/** - * Shows Live TV settings. - */ +/** Shows Live TV settings. */ public class SettingsFragment extends SideFragment { private static final String TRACKER_LABEL = "settings"; @@ -58,58 +54,73 @@ public class SettingsFragment extends SideFragment { @Override protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); - final Item customizeChannelListItem = new SubMenuItem( - getString(R.string.settings_channel_source_item_customize_channels), - getString(R.string.settings_channel_source_item_customize_channels_description), - getMainActivity().getOverlayManager().getSideFragmentManager()) { - @Override - protected SideFragment getFragment() { - return new CustomizeChannelListFragment(); - } + final Item customizeChannelListItem = + new SubMenuItem( + getString(R.string.settings_channel_source_item_customize_channels), + getString( + R.string + .settings_channel_source_item_customize_channels_description), + getMainActivity().getOverlayManager().getSideFragmentManager()) { + @Override + protected SideFragment getFragment() { + return new CustomizeChannelListFragment(); + } - @Override - protected void onBind(View view) { - super.onBind(view); - setEnabled(false); - } + @Override + protected void onBind(View view) { + super.onBind(view); + setEnabled(false); + } - @Override - protected void onUpdate() { - super.onUpdate(); - setEnabled(getChannelDataManager().getChannelCount() != 0); - } - }; + @Override + protected void onUpdate() { + super.onUpdate(); + setEnabled(getChannelDataManager().getChannelCount() != 0); + } + }; customizeChannelListItem.setEnabled(false); items.add(customizeChannelListItem); final MainActivity activity = getMainActivity(); - boolean hasNewInput = SetupUtils.getInstance(activity) - .hasNewInput(activity.getTvInputManagerHelper()); - items.add(new ActionItem( - getString(R.string.settings_channel_source_item_setup), - hasNewInput ? getString(R.string.settings_channel_source_item_setup_new_inputs) - : null) { - @Override - protected void onSelected() { - closeFragment(); - activity.getOverlayManager().showSetupFragment(); - } - }); + boolean hasNewInput = + TvSingletons.getSingletons(getContext()) + .getSetupUtils() + .hasNewInput(activity.getTvInputManagerHelper()); + items.add( + new ActionItem( + getString(R.string.settings_channel_source_item_setup), + hasNewInput + ? getString(R.string.settings_channel_source_item_setup_new_inputs) + : null) { + @Override + protected void onSelected() { + closeFragment(); + activity.getOverlayManager().showSetupFragment(); + } + }); if (PermissionUtils.hasModifyParentalControls(getMainActivity())) { - items.add(new ActionItem( - getString(R.string.settings_parental_controls), getString( - activity.getParentalControlSettings().isParentalControlsEnabled() - ? R.string.option_toggle_parental_controls_on - : R.string.option_toggle_parental_controls_off)) { - @Override - protected void onSelected() { - getMainActivity().getOverlayManager() - .getSideFragmentManager().hideSidePanel(true); - PinDialogFragment fragment = PinDialogFragment - .create(PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN); - getMainActivity().getOverlayManager() - .showDialogFragment(PinDialogFragment.DIALOG_TAG, fragment, true); - } - }); + items.add( + new ActionItem( + getString(R.string.settings_parental_controls), + getString( + activity.getParentalControlSettings() + .isParentalControlsEnabled() + ? R.string.option_toggle_parental_controls_on + : R.string.option_toggle_parental_controls_off)) { + @Override + protected void onSelected() { + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() + .hideSidePanel(true); + PinDialogFragment fragment = + PinDialogFragment.create( + PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN); + getMainActivity() + .getOverlayManager() + .showDialogFragment( + PinDialogFragment.DIALOG_TAG, fragment, true); + } + }); } else { // Note: parental control is turned off, when MODIFY_PARENTAL_CONTROLS is not granted. // But, we may be able to turn on channel lock feature regardless of the permission. @@ -117,8 +128,10 @@ public class SettingsFragment extends SideFragment { } boolean showTrickplaySetting = false; if (TUNER.isEnabled(getContext())) { - for (TvInputInfo inputInfo : TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getTvInputInfos(true, true)) { + for (TvInputInfo inputInfo : + TvSingletons.getSingletons(getContext()) + .getTvInputManagerHelper() + .getTvInputInfos(true, true)) { if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) { showTrickplaySetting = true; break; @@ -126,46 +139,51 @@ public class SettingsFragment extends SideFragment { } if (showTrickplaySetting) { showTrickplaySetting = - TvCustomizationManager.getTrickplayMode(getContext()) - == TvCustomizationManager.TRICKPLAY_MODE_ENABLED; + CustomizationManager.getTrickplayMode(getContext()) + == CustomizationManager.TRICKPLAY_MODE_ENABLED; } } if (showTrickplaySetting) { items.add( - new SwitchItem(getString(R.string.settings_trickplay), + new SwitchItem( + getString(R.string.settings_trickplay), getString(R.string.settings_trickplay), getString(R.string.settings_trickplay_description), getResources().getInteger(R.integer.trickplay_description_max_lines)) { @Override protected void onUpdate() { super.onUpdate(); - boolean enabled = TunerPreferences.getTrickplaySetting(getContext()) - != TunerPreferences.TRICKPLAY_SETTING_DISABLED; + boolean enabled = + CommonPreferences.getTrickplaySetting(getContext()) + != CommonPreferences.TRICKPLAY_SETTING_DISABLED; setChecked(enabled); } @Override protected void onSelected() { super.onSelected(); - @TunerPreferences.TrickplaySetting int setting = - isChecked() ? TunerPreferences.TRICKPLAY_SETTING_ENABLED - : TunerPreferences.TRICKPLAY_SETTING_DISABLED; - TunerPreferences.setTrickplaySetting(getContext(), setting); + @CommonPreferences.TrickplaySetting + int setting = + isChecked() + ? CommonPreferences.TRICKPLAY_SETTING_ENABLED + : CommonPreferences.TRICKPLAY_SETTING_DISABLED; + CommonPreferences.setTrickplaySetting(getContext(), setting); } }); } - items.add(new ActionItem(getString(R.string.settings_send_feedback)) { - @Override - protected void onSelected() { - Intent intent = new Intent(Intent.ACTION_APP_ERROR); - ApplicationErrorReport report = new ApplicationErrorReport(); - report.packageName = report.processName = getContext().getPackageName(); - report.time = System.currentTimeMillis(); - report.type = ApplicationErrorReport.TYPE_NONE; - intent.putExtra(Intent.EXTRA_BUG_REPORT, report); - startActivityForResult(intent, 0); - } - }); + items.add( + new ActionItem(getString(R.string.settings_send_feedback)) { + @Override + protected void onSelected() { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = getContext().getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_NONE; + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + startActivityForResult(intent, 0); + } + }); if (Licenses.hasLicenses(getContext())) { items.add( new SubMenuItem( @@ -178,8 +196,10 @@ public class SettingsFragment extends SideFragment { }); } // Show version. - SimpleActionItem version = new SimpleActionItem(getString(R.string.settings_menu_version), - ((TvApplication) activity.getApplicationContext()).getVersionName()); + SimpleActionItem version = + new SimpleActionItem( + getString(R.string.settings_menu_version), + ((TvApplication) activity.getApplicationContext()).getVersionName()); version.setClickable(false); items.add(version); return items; diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index 6bd921a2..2902ea7f 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -28,18 +28,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.util.DurationTimer; +import com.android.tv.TvSingletons; import com.android.tv.analytics.HasTrackerLabel; import com.android.tv.analytics.Tracker; +import com.android.tv.common.util.DurationTimer; +import com.android.tv.common.util.SystemProperties; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ProgramDataManager; -import com.android.tv.util.SystemProperties; import com.android.tv.util.ViewCache; - import java.util.List; public abstract class SideFragment<T extends Item> extends Fragment implements HasTrackerLabel { @@ -74,8 +72,8 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H /** * @param hideKey the KeyCode used to hide the fragment - * @param debugHideKey the KeyCode used to hide the fragment if - * {@link SystemProperties#USE_DEBUG_KEYS}. + * @param debugHideKey the KeyCode used to hide the fragment if {@link + * SystemProperties#USE_DEBUG_KEYS}. */ public SideFragment(int hideKey, int debugHideKey) { mHideKey = hideKey; @@ -87,14 +85,15 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H super.onAttach(context); mChannelDataManager = getMainActivity().getChannelDataManager(); mProgramDataManager = getMainActivity().getProgramDataManager(); - mTracker = TvApplication.getSingletons(context).getTracker(); + mTracker = TvSingletons.getSingletons(context).getTracker(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = ViewCache.getInstance().getOrCreateView( - inflater, getFragmentLayoutResourceId(), container); + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = + ViewCache.getInstance() + .getOrCreateView(inflater, getFragmentLayoutResourceId(), container); TextView textView = (TextView) view.findViewById(R.id.side_panel_title); textView.setText(getTitle()); @@ -131,8 +130,8 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H public final boolean isHideKeyForThisPanel(int keyCode) { boolean debugKeysEnabled = SystemProperties.USE_DEBUG_KEYS.getValue(); - return mHideKey != KeyEvent.KEYCODE_UNKNOWN && - (mHideKey == keyCode || (debugKeysEnabled && mDebugHideKey == keyCode)); + return mHideKey != KeyEvent.KEYCODE_UNKNOWN + && (mHideKey == keyCode || (debugKeysEnabled && mDebugHideKey == keyCode)); } @Override @@ -195,16 +194,14 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H item.notifyUpdated(); } - /** - * Notifies all items of ItemAdapter has changed without structural changes. - */ + /** Notifies all items of ItemAdapter has changed without structural changes. */ protected void notifyItemsChanged() { notifyItemsChanged(0, mAdapter.getItemCount()); } /** - * Notifies some items of ItemAdapter has changed starting from position - * <code>positionStart</code> to the end without structural changes. + * Notifies some items of ItemAdapter has changed starting from position <code>positionStart + * </code> to the end without structural changes. */ protected void notifyItemsChanged(int positionStart) { notifyItemsChanged(positionStart, mAdapter.getItemCount() - positionStart); @@ -225,20 +222,20 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H } protected abstract String getTitle(); + @Override public abstract String getTrackerLabel(); + protected abstract List<T> getItemList(); public interface SideFragmentListener { void onSideFragmentViewDestroyed(); } - /** - * Preloads the item views. - */ + /** Preloads the item views. */ public static void preloadItemViews(Context context) { - ViewCache.getInstance().putView( - context, R.layout.option_fragment, new FrameLayout(context), 1); + ViewCache.getInstance() + .putView(context, R.layout.option_fragment, new FrameLayout(context), 1); VerticalGridView fakeParent = new VerticalGridView(context); for (int id : PRELOAD_VIEW_IDS) { sRecycledViewPool.setMaxRecycledViews(id, PRELOAD_VIEW_SIZE); @@ -246,9 +243,7 @@ public abstract class SideFragment<T extends Item> extends Fragment implements H } } - /** - * Releases the recycled view pool. - */ + /** Releases the recycled view pool. */ public static void releaseRecycledViewPool() { sRecycledViewPool.clear(); } diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java index d02d3fb7..5bba4097 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java +++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java @@ -22,13 +22,14 @@ import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.FragmentManager; import android.app.FragmentTransaction; -import android.os.Handler; import android.view.View; import android.view.ViewTreeObserver; - +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import com.android.tv.R; +import com.android.tv.ui.hideable.AutoHideScheduler; -public class SideFragmentManager { +/** Manages {@link SideFragment}s. */ +public class SideFragmentManager implements AccessibilityStateChangeListener { private static final String FIRST_BACKSTACK_RECORD_NAME = "0"; private final Activity mActivity; @@ -45,17 +46,11 @@ public class SideFragmentManager { private final Animator mShowAnimator; private final Animator mHideAnimator; - private final Handler mHandler = new Handler(); - private final Runnable mHideAllRunnable = new Runnable() { - @Override - public void run() { - hideAll(true); - } - }; + private final AutoHideScheduler mAutoHideScheduler; private final long mShowDurationMillis; - public SideFragmentManager(Activity activity, Runnable preShowRunnable, - Runnable postHideRunnable) { + public SideFragmentManager( + Activity activity, Runnable preShowRunnable, Runnable postHideRunnable) { mActivity = activity; mFragmentManager = mActivity.getFragmentManager(); mPreShowRunnable = preShowRunnable; @@ -66,16 +61,18 @@ public class SideFragmentManager { mShowAnimator.setTarget(mPanel); mHideAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit); mHideAnimator.setTarget(mPanel); - mHideAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Animation is still in running state at this point. - hideAllInternal(); - } - }); + mHideAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Animation is still in running state at this point. + hideAllInternal(); + } + }); - mShowDurationMillis = mActivity.getResources().getInteger( - R.integer.side_panel_show_duration); + mShowDurationMillis = + mActivity.getResources().getInteger(R.integer.side_panel_show_duration); + mAutoHideScheduler = new AutoHideScheduler(activity, () -> hideAll(true)); } public int getCount() { @@ -90,16 +87,12 @@ public class SideFragmentManager { return mHideAnimator.isStarted(); } - /** - * Shows the given {@link SideFragment}. - */ + /** Shows the given {@link SideFragment}. */ public void show(SideFragment sideFragment) { show(sideFragment, true); } - /** - * Shows the given {@link SideFragment}. - */ + /** Shows the given {@link SideFragment}. */ public void show(SideFragment sideFragment, boolean showEnterAnimation) { if (isHiding()) { mHideAnimator.end(); @@ -114,7 +107,8 @@ public class SideFragmentManager { R.animator.side_panel_fragment_pop_exit); } ft.replace(R.id.side_fragment_container, sideFragment) - .addToBackStack(Integer.toString(mFragmentCount)).commit(); + .addToBackStack(Integer.toString(mFragmentCount)) + .commit(); mFragmentCount++; if (isFirst) { @@ -122,17 +116,18 @@ public class SideFragmentManager { // slide-in animation to prevent jankiness resulted by performing transition and // layouting at the same time with animation. mPanel.setVisibility(View.VISIBLE); - mShowOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this); - mShowOnGlobalLayoutListener = null; - if (mPreShowRunnable != null) { - mPreShowRunnable.run(); - } - mShowAnimator.start(); - } - }; + mShowOnGlobalLayoutListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mShowOnGlobalLayoutListener = null; + if (mPreShowRunnable != null) { + mPreShowRunnable.run(); + } + mShowAnimator.start(); + } + }; mPanel.getViewTreeObserver().addOnGlobalLayoutListener(mShowOnGlobalLayoutListener); } scheduleHideAll(); @@ -177,14 +172,14 @@ public class SideFragmentManager { } private void hideAllInternal() { - mHandler.removeCallbacksAndMessages(null); + mAutoHideScheduler.cancel(); if (mFragmentCount == 0) { return; } mPanel.setVisibility(View.GONE); - mFragmentManager.popBackStack(FIRST_BACKSTACK_RECORD_NAME, - FragmentManager.POP_BACK_STACK_INCLUSIVE); + mFragmentManager.popBackStack( + FIRST_BACKSTACK_RECORD_NAME, FragmentManager.POP_BACK_STACK_INCLUSIVE); mFragmentCount = 0; if (mPostHideRunnable != null) { @@ -193,8 +188,8 @@ public class SideFragmentManager { } /** - * Show the side panel with animation. If there are many entries in the fragment stack, - * the animation look like that there's only one fragment. + * Show the side panel with animation. If there are many entries in the fragment stack, the + * animation look like that there's only one fragment. * * @param withAnimation specifies if animation should be shown. */ @@ -211,22 +206,23 @@ public class SideFragmentManager { } /** - * Hide the side panel. This method just hide the panel and preserves the back - * stack. If you want to empty the back stack, call {@link #hideAll}. + * Hide the side panel. This method just hide the panel and preserves the back stack. If you + * want to empty the back stack, call {@link #hideAll}. */ public void hideSidePanel(boolean withAnimation) { - mHandler.removeCallbacks(mHideAllRunnable); + mAutoHideScheduler.cancel(); if (withAnimation) { Animator hideAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit); hideAnimator.setTarget(mPanel); hideAnimator.start(); - hideAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mPanel.setVisibility(View.GONE); - } - }); + hideAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mPanel.setVisibility(View.GONE); + } + }); } else { mPanel.setVisibility(View.GONE); } @@ -236,23 +232,23 @@ public class SideFragmentManager { return mPanel.getVisibility() == View.VISIBLE; } - /** - * Resets the timer for hiding side fragment. - */ + /** Resets the timer for hiding side fragment. */ public void scheduleHideAll() { - mHandler.removeCallbacks(mHideAllRunnable); - mHandler.postDelayed(mHideAllRunnable, mShowDurationMillis); + mAutoHideScheduler.schedule(mShowDurationMillis); } - /** - * Should {@code keyCode} hide the current panel. - */ + /** Should {@code keyCode} hide the current panel. */ public boolean isHideKeyForCurrentPanel(int keyCode) { if (isActive()) { - SideFragment current = (SideFragment) mFragmentManager.findFragmentById( - R.id.side_fragment_container); + SideFragment current = + (SideFragment) mFragmentManager.findFragmentById(R.id.side_fragment_container); return current != null && current.isHideKeyForThisPanel(keyCode); } return false; } + + @Override + public void onAccessibilityStateChanged(boolean enabled) { + mAutoHideScheduler.onAccessibilityStateChanged(enabled); + } } diff --git a/src/com/android/tv/ui/sidepanel/SimpleActionItem.java b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java index 42553b66..4457086d 100644 --- a/src/com/android/tv/ui/sidepanel/SimpleActionItem.java +++ b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java @@ -16,9 +16,7 @@ package com.android.tv.ui.sidepanel; -/** - * A simple item which shows title and description. - */ +/** A simple item which shows title and description. */ public class SimpleActionItem extends ActionItem { public SimpleActionItem(String title) { super(title); @@ -29,6 +27,5 @@ public class SimpleActionItem extends ActionItem { } @Override - protected void onSelected() { - } + protected void onSelected() {} } diff --git a/src/com/android/tv/ui/sidepanel/SubMenuItem.java b/src/com/android/tv/ui/sidepanel/SubMenuItem.java index 4b0e8e2c..5e4c77ca 100644 --- a/src/com/android/tv/ui/sidepanel/SubMenuItem.java +++ b/src/com/android/tv/ui/sidepanel/SubMenuItem.java @@ -16,7 +16,6 @@ package com.android.tv.ui.sidepanel; - public abstract class SubMenuItem extends ActionItem { private final SideFragmentManager mSideFragmentManager; diff --git a/src/com/android/tv/ui/sidepanel/SwitchItem.java b/src/com/android/tv/ui/sidepanel/SwitchItem.java index 06591b62..8b5e1b64 100644 --- a/src/com/android/tv/ui/sidepanel/SwitchItem.java +++ b/src/com/android/tv/ui/sidepanel/SwitchItem.java @@ -31,8 +31,8 @@ public class SwitchItem extends CompoundButtonItem { super(checkedTitle, uncheckedTitle, description); } - public SwitchItem(String checkedTitle, String uncheckedTitle, String description, - int maxLines) { + public SwitchItem( + String checkedTitle, String uncheckedTitle, String description, int maxLines) { super(checkedTitle, uncheckedTitle, description, maxLines); } @@ -50,4 +50,4 @@ public class SwitchItem extends CompoundButtonItem { protected void onSelected() { setChecked(!isChecked()); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java index ede5c268..4e3cf7fb 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java @@ -19,6 +19,8 @@ package com.android.tv.ui.sidepanel.parentalcontrols; import android.database.ContentObserver; import android.media.tv.TvContract; import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.widget.VerticalGridView; @@ -27,17 +29,16 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import com.android.tv.R; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelNumber; +import com.android.tv.data.api.Channel; +import com.android.tv.recommendation.ChannelPreviewUpdater; import com.android.tv.ui.OnRepeatedKeyInterceptListener; import com.android.tv.ui.sidepanel.ActionItem; import com.android.tv.ui.sidepanel.ChannelCheckItem; import com.android.tv.ui.sidepanel.DividerItem; import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -49,45 +50,55 @@ public class ChannelsBlockedFragment extends SideFragment { private final List<Channel> mChannels = new ArrayList<>(); private long mLastFocusedChannelId = Channel.INVALID_ID; private int mSelectedPosition = INVALID_POSITION; - private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange, Uri uri) { - notifyItemsChanged(); - } - }; + private boolean mUpdated; + private final ContentObserver mProgramUpdateObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange, Uri uri) { + notifyItemsChanged(); + } + }; private final Item mLockAllItem = new BlockAllItem(); private final List<Item> mItems = new ArrayList<>(); @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); if (mSelectedPosition != INVALID_POSITION) { setSelectedPosition(mSelectedPosition); } VerticalGridView listView = (VerticalGridView) view.findViewById(R.id.side_panel_list); - listView.setOnKeyInterceptListener(new OnRepeatedKeyInterceptListener(listView) { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - // In order to send tune operation once for continuous channel up/down events, - // we only call the moveToChannel method on ACTION_UP event of channel switch keys. - if (event.getAction() == KeyEvent.ACTION_UP) { - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - if (mLastFocusedChannelId != Channel.INVALID_ID) { - getMainActivity().tuneToChannel( - getChannelDataManager().getChannel(mLastFocusedChannelId)); + listView.setOnKeyInterceptListener( + new OnRepeatedKeyInterceptListener(listView) { + @Override + public boolean onInterceptKeyEvent(KeyEvent event) { + // In order to send tune operation once for continuous channel up/down + // events, + // we only call the moveToChannel method on ACTION_UP event of channel + // switch keys. + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mLastFocusedChannelId != Channel.INVALID_ID) { + getMainActivity() + .tuneToChannel( + getChannelDataManager() + .getChannel(mLastFocusedChannelId)); + } + break; } - break; + } + return super.onInterceptKeyEvent(event); } - } - return super.onInterceptKeyEvent(event); - } - }); - getActivity().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, - true, mProgramUpdateObserver); + }); + getActivity() + .getContentResolver() + .registerContentObserver( + TvContract.Programs.CONTENT_URI, true, mProgramUpdateObserver); getMainActivity().startShrunkenTvView(true, true); + mUpdated = false; return view; } @@ -96,6 +107,10 @@ public class ChannelsBlockedFragment extends SideFragment { getActivity().getContentResolver().unregisterContentObserver(mProgramUpdateObserver); getChannelDataManager().applyUpdatedValuesToDb(); getMainActivity().endShrunkenTvView(); + if (VERSION.SDK_INT >= VERSION_CODES.O && mUpdated) { + ChannelPreviewUpdater.getInstance(getMainActivity()) + .updatePreviewDataForChannelsImmediately(); + } super.onDestroyView(); } @@ -115,22 +130,24 @@ public class ChannelsBlockedFragment extends SideFragment { mItems.add(mLockAllItem); mChannels.clear(); mChannels.addAll(getChannelDataManager().getChannelList()); - Collections.sort(mChannels, new Comparator<Channel>() { - @Override - public int compare(Channel lhs, Channel rhs) { - if (lhs.isBrowsable() != rhs.isBrowsable()) { - return lhs.isBrowsable() ? -1 : 1; - } - return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); - } - }); + Collections.sort( + mChannels, + new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + if (lhs.isBrowsable() != rhs.isBrowsable()) { + return lhs.isBrowsable() ? -1 : 1; + } + return ChannelNumber.compare( + lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } + }); final long currentChannelId = getMainActivity().getCurrentChannelId(); boolean hasHiddenChannels = false; for (Channel channel : mChannels) { if (!channel.isBrowsable() && !hasHiddenChannels) { - mItems.add(new DividerItem( - getString(R.string.option_channels_subheader_hidden))); + mItems.add(new DividerItem(getString(R.string.option_channels_subheader_hidden))); hasHiddenChannels = true; } mItems.add(new ChannelBlockedItem(channel)); @@ -177,6 +194,7 @@ public class ChannelsBlockedFragment extends SideFragment { } mBlockedChannelCount = lock ? mChannels.size() : 0; notifyItemsChanged(); + mUpdated = true; } @Override @@ -186,8 +204,11 @@ public class ChannelsBlockedFragment extends SideFragment { } private void updateText() { - mTextView.setText(getString(areAllChannelsBlocked() ? - R.string.option_channels_unlock_all : R.string.option_channels_lock_all)); + mTextView.setText( + getString( + areAllChannelsBlocked() + ? R.string.option_channels_unlock_all + : R.string.option_channels_lock_all)); } private boolean areAllChannelsBlocked() { @@ -217,6 +238,7 @@ public class ChannelsBlockedFragment extends SideFragment { getChannelDataManager().updateLocked(getChannel().getId(), isChecked()); mBlockedChannelCount += isChecked() ? 1 : -1; notifyItemChanged(mLockAllItem); + mUpdated = true; } @Override diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java index 9a4879fc..1c91ba9b 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java @@ -18,17 +18,15 @@ package com.android.tv.ui.sidepanel.parentalcontrols; import android.view.View; import android.widget.TextView; - import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.Channel; +import com.android.tv.data.api.Channel; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.ui.sidepanel.ActionItem; import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.ui.sidepanel.SubMenuItem; import com.android.tv.ui.sidepanel.SwitchItem; - import java.util.ArrayList; import java.util.List; @@ -36,12 +34,13 @@ public class ParentalControlsFragment extends SideFragment { private static final String TRACKER_LABEL = "Parental controls"; private List<ActionItem> mActionItems; - private final SideFragmentListener mSideFragmentListener = new SideFragmentListener() { - @Override - public void onSideFragmentViewDestroyed() { - notifyDataSetChanged(); - } - }; + private final SideFragmentListener mSideFragmentListener = + new SideFragmentListener() { + @Override + public void onSideFragmentViewDestroyed() { + notifyDataSetChanged(); + } + }; @Override protected String getTitle() { @@ -56,89 +55,103 @@ public class ParentalControlsFragment extends SideFragment { @Override protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); - items.add(new SwitchItem(getString(R.string.option_toggle_parental_controls_on), - getString(R.string.option_toggle_parental_controls_off)) { - @Override - protected void onUpdate() { - super.onUpdate(); - setChecked(getMainActivity().getParentalControlSettings() - .isParentalControlsEnabled()); - } + items.add( + new SwitchItem( + getString(R.string.option_toggle_parental_controls_on), + getString(R.string.option_toggle_parental_controls_off)) { + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked( + getMainActivity() + .getParentalControlSettings() + .isParentalControlsEnabled()); + } - @Override - protected void onSelected() { - super.onSelected(); - boolean checked = isChecked(); - getMainActivity().getParentalControlSettings().setParentalControlsEnabled(checked); - enableActionItems(checked); - } - }); + @Override + protected void onSelected() { + super.onSelected(); + boolean checked = isChecked(); + getMainActivity() + .getParentalControlSettings() + .setParentalControlsEnabled(checked); + enableActionItems(checked); + } + }); mActionItems = new ArrayList<>(); - mActionItems.add(new SubMenuItem(getString(R.string.option_channels_locked), "", - getMainActivity().getOverlayManager().getSideFragmentManager()) { - TextView mDescriptionView; - - @Override - protected SideFragment getFragment() { - SideFragment fragment = new ChannelsBlockedFragment(); - fragment.setListener(mSideFragmentListener); - return fragment; - } + mActionItems.add( + new SubMenuItem( + getString(R.string.option_channels_locked), + "", + getMainActivity().getOverlayManager().getSideFragmentManager()) { + TextView mDescriptionView; + + @Override + protected SideFragment getFragment() { + SideFragment fragment = new ChannelsBlockedFragment(); + fragment.setListener(mSideFragmentListener); + return fragment; + } - @Override - protected void onBind(View view) { - super.onBind(view); - mDescriptionView = (TextView) view.findViewById(R.id.description); - } + @Override + protected void onBind(View view) { + super.onBind(view); + mDescriptionView = (TextView) view.findViewById(R.id.description); + } - @Override - protected void onUpdate() { - super.onUpdate(); - int lockedAndBrowsableChannelCount = 0; - for (Channel channel : getChannelDataManager().getChannelList()) { - if (channel.isLocked() && channel.isBrowsable()) { - ++lockedAndBrowsableChannelCount; + @Override + protected void onUpdate() { + super.onUpdate(); + int lockedAndBrowsableChannelCount = 0; + for (Channel channel : getChannelDataManager().getChannelList()) { + if (channel.isLocked() && channel.isBrowsable()) { + ++lockedAndBrowsableChannelCount; + } + } + if (lockedAndBrowsableChannelCount > 0) { + mDescriptionView.setText( + Integer.toString(lockedAndBrowsableChannelCount)); + } else { + mDescriptionView.setText( + getMainActivity().getString(R.string.option_no_locked_channel)); + } } - } - if (lockedAndBrowsableChannelCount > 0) { - mDescriptionView.setText(Integer.toString(lockedAndBrowsableChannelCount)); - } else { - mDescriptionView.setText( - getMainActivity().getString(R.string.option_no_locked_channel)); - } - } - @Override - protected void onUnbind() { - super.onUnbind(); - mDescriptionView = null; - } - }); - mActionItems.add(new SubMenuItem(getString(R.string.option_program_restrictions), - ProgramRestrictionsFragment.getDescription(getMainActivity()), - getMainActivity().getOverlayManager().getSideFragmentManager()) { - @Override - protected SideFragment getFragment() { - SideFragment fragment = new ProgramRestrictionsFragment(); - fragment.setListener(mSideFragmentListener); - return fragment; - } - }); - mActionItems.add(new ActionItem(getString(R.string.option_change_pin)) { - @Override - protected void onSelected() { - final MainActivity tvActivity = getMainActivity(); - tvActivity.getOverlayManager().getSideFragmentManager().hideSidePanel(true); - PinDialogFragment fragment = PinDialogFragment.create( - PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN); - tvActivity.getOverlayManager().showDialogFragment(PinDialogFragment.DIALOG_TAG, - fragment, true); - } - }); + @Override + protected void onUnbind() { + super.onUnbind(); + mDescriptionView = null; + } + }); + mActionItems.add( + new SubMenuItem( + getString(R.string.option_program_restrictions), + ProgramRestrictionsFragment.getDescription(getMainActivity()), + getMainActivity().getOverlayManager().getSideFragmentManager()) { + @Override + protected SideFragment getFragment() { + SideFragment fragment = new ProgramRestrictionsFragment(); + fragment.setListener(mSideFragmentListener); + return fragment; + } + }); + mActionItems.add( + new ActionItem(getString(R.string.option_change_pin)) { + @Override + protected void onSelected() { + final MainActivity tvActivity = getMainActivity(); + tvActivity.getOverlayManager().getSideFragmentManager().hideSidePanel(true); + PinDialogFragment fragment = + PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN); + tvActivity + .getOverlayManager() + .showDialogFragment(PinDialogFragment.DIALOG_TAG, fragment, true); + } + }); items.addAll(mActionItems); - enableActionItems(getMainActivity().getParentalControlSettings() - .isParentalControlsEnabled()); + enableActionItems( + getMainActivity().getParentalControlSettings().isParentalControlsEnabled()); return items; } diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java index 1df7fe59..ead3c105 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java @@ -21,19 +21,19 @@ import com.android.tv.R; import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.ui.sidepanel.SubMenuItem; - import java.util.ArrayList; import java.util.List; public class ProgramRestrictionsFragment extends SideFragment { private static final String TRACKER_LABEL = "Program restrictions"; - private final SideFragmentListener mSideFragmentListener = new SideFragmentListener() { - @Override - public void onSideFragmentViewDestroyed() { - notifyDataSetChanged(); - } - }; + private final SideFragmentListener mSideFragmentListener = + new SideFragmentListener() { + @Override + public void onSideFragmentViewDestroyed() { + notifyDataSetChanged(); + } + }; public static String getDescription(MainActivity tvActivity) { return RatingsFragment.getDescription(tvActivity); @@ -53,33 +53,37 @@ public class ProgramRestrictionsFragment extends SideFragment { protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); - items.add(new SubMenuItem(getString(R.string.option_country_rating_systems), - RatingSystemsFragment.getDescription(getMainActivity()), - getMainActivity().getOverlayManager().getSideFragmentManager()) { - @Override - protected SideFragment getFragment() { - SideFragment fragment = new RatingSystemsFragment(); - fragment.setListener(mSideFragmentListener); - return fragment; - } - }); + items.add( + new SubMenuItem( + getString(R.string.option_country_rating_systems), + RatingSystemsFragment.getDescription(getMainActivity()), + getMainActivity().getOverlayManager().getSideFragmentManager()) { + @Override + protected SideFragment getFragment() { + SideFragment fragment = new RatingSystemsFragment(); + fragment.setListener(mSideFragmentListener); + return fragment; + } + }); String ratingsDescription = RatingsFragment.getDescription(getMainActivity()); - SubMenuItem ratingsItem = new SubMenuItem(getString(R.string.option_ratings), - ratingsDescription, - getMainActivity().getOverlayManager().getSideFragmentManager()) { - @Override - protected SideFragment getFragment() { - SideFragment fragment = new RatingsFragment(); - fragment.setListener(mSideFragmentListener); - return fragment; - } - }; + SubMenuItem ratingsItem = + new SubMenuItem( + getString(R.string.option_ratings), + ratingsDescription, + getMainActivity().getOverlayManager().getSideFragmentManager()) { + @Override + protected SideFragment getFragment() { + SideFragment fragment = new RatingsFragment(); + fragment.setListener(mSideFragmentListener); + return fragment; + } + }; // When "None" is selected for rating systems, disable the Ratings option. - if (RatingSystemsFragment.getDescription(getMainActivity()).equals( - getString(R.string.option_no_enabled_rating_system))) { + if (RatingSystemsFragment.getDescription(getMainActivity()) + .equals(getString(R.string.option_no_enabled_rating_system))) { ratingsItem.setEnabled(false); } items.add(ratingsItem); return items; } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingSystemsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingSystemsFragment.java index ba9adc50..e0f546f9 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingSystemsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingSystemsFragment.java @@ -17,7 +17,6 @@ package com.android.tv.ui.sidepanel.parentalcontrols; import android.os.Bundle; - import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.parental.ContentRatingSystem; @@ -28,7 +27,6 @@ import com.android.tv.ui.sidepanel.CheckBoxItem; import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.util.TvSettings; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -57,7 +55,8 @@ public class RatingSystemsFragment extends SideFragment { builder.append(s.getDisplayName()); builder.append(", "); } - return builder.length() > 0 ? builder.substring(0, builder.length() - 2) + return builder.length() > 0 + ? builder.substring(0, builder.length() - 2) : tvActivity.getString(R.string.option_no_enabled_rating_system); } @@ -85,7 +84,8 @@ public class RatingSystemsFragment extends SideFragment { // Add default, custom and preselected content rating systems to the "short" list. for (ContentRatingSystem s : contentRatingSystems) { - if (!s.isCustom() && s.getCountries() != null + if (!s.isCustom() + && s.getCountries() != null && s.getCountries().contains(Locale.getDefault().getCountry())) { items.add(new RatingSystemItem(s)); } else if (s.isCustom() || parentalControlSettings.isContentRatingSystemEnabled(s)) { @@ -117,12 +117,13 @@ public class RatingSystemsFragment extends SideFragment { allItems.addAll(itemsHiddenMultipleCountries); // Add "See All" to the "short" list. - items.add(new ActionItem(getString(R.string.option_see_all_rating_systems)) { - @Override - protected void onSelected() { - setItems(allItems); - } - }); + items.add( + new ActionItem(getString(R.string.option_see_all_rating_systems)) { + @Override + protected void onSelected() { + setItems(allItems); + } + }); return items; } @@ -136,7 +137,8 @@ public class RatingSystemsFragment extends SideFragment { ContentRatingsManager manager = tvActivity.getContentRatingsManager(); ParentalControlSettings settings = tvActivity.getParentalControlSettings(); for (ContentRatingSystem s : contentRatingSystems) { - if (!s.isCustom() && s.getCountries() != null + if (!s.isCustom() + && s.getCountries() != null && s.getCountries().contains(Locale.getDefault().getCountry())) { settings.setContentRatingSystemEnabled(manager, s, true); } @@ -158,16 +160,21 @@ public class RatingSystemsFragment extends SideFragment { @Override protected void onUpdate() { super.onUpdate(); - setChecked(getMainActivity().getParentalControlSettings() - .isContentRatingSystemEnabled(mContentRatingSystem)); + setChecked( + getMainActivity() + .getParentalControlSettings() + .isContentRatingSystemEnabled(mContentRatingSystem)); } @Override protected void onSelected() { super.onSelected(); - getMainActivity().getParentalControlSettings().setContentRatingSystemEnabled( - getMainActivity().getContentRatingsManager(), mContentRatingSystem, - isChecked()); + getMainActivity() + .getParentalControlSettings() + .setContentRatingSystemEnabled( + getMainActivity().getContentRatingsManager(), + mContentRatingSystem, + isChecked()); } } } diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java index 7c8cecbe..128fcd1a 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java @@ -26,8 +26,8 @@ import android.widget.CompoundButton; import android.widget.ImageView; import com.android.tv.MainActivity; import com.android.tv.R; +import com.android.tv.common.experiments.Experiments; import com.android.tv.dialog.WebDialogFragment; -import com.android.tv.experiments.Experiments; import com.android.tv.license.LicenseUtils; import com.android.tv.parental.ContentRatingSystem; import com.android.tv.parental.ContentRatingSystem.Rating; @@ -52,26 +52,23 @@ public class RatingsFragment extends SideFragment { static { sLevelResourceIdMap = new SparseIntArray(5); - sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_NONE, - R.string.option_rating_none); - sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_HIGH, - R.string.option_rating_high); - sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_MEDIUM, - R.string.option_rating_medium); - sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_LOW, - R.string.option_rating_low); - sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_CUSTOM, - R.string.option_rating_custom); + sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_NONE, R.string.option_rating_none); + sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high); + sLevelResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium); + sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low); + sLevelResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom); sDescriptionResourceIdMap = new SparseIntArray(sLevelResourceIdMap.size()); - sDescriptionResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_HIGH, - R.string.option_rating_high_description); - sDescriptionResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_MEDIUM, - R.string.option_rating_medium_description); - sDescriptionResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_LOW, - R.string.option_rating_low_description); - sDescriptionResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_CUSTOM, - R.string.option_rating_custom_description); + sDescriptionResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_HIGH, R.string.option_rating_high_description); + sDescriptionResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_MEDIUM, R.string.option_rating_medium_description); + sDescriptionResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_LOW, R.string.option_rating_low_description); + sDescriptionResourceIdMap.put( + TvSettings.CONTENT_RATING_LEVEL_CUSTOM, R.string.option_rating_custom_description); } private final List<RatingLevelItem> mRatingLevelItems = new ArrayList<>(); @@ -81,8 +78,8 @@ public class RatingsFragment extends SideFragment { private ParentalControlSettings mParentalControlSettings; public static String getDescription(MainActivity tvActivity) { - @ContentRatingLevel int currentLevel = - tvActivity.getParentalControlSettings().getContentRatingLevel(); + @ContentRatingLevel + int currentLevel = tvActivity.getParentalControlSettings().getContentRatingLevel(); if (sLevelResourceIdMap.indexOfKey(currentLevel) >= 0) { return tvActivity.getString(sLevelResourceIdMap.get(currentLevel)); } @@ -128,9 +125,10 @@ public class RatingsFragment extends SideFragment { boolean hasSubRating = false; items.add(new DividerItem(s.getDisplayName())); for (Rating rating : s.getRatings()) { - RatingItem item = rating.getSubRatings().isEmpty() ? - new RatingItem(s, rating) : - new RatingWithSubItem(s, rating); + RatingItem item = + rating.getSubRatings().isEmpty() + ? new RatingItem(s, rating) + : new RatingWithSubItem(s, rating); items.add(item); if (rating.getSubRatings().isEmpty()) { ratingItems.add(item); @@ -201,15 +199,18 @@ public class RatingsFragment extends SideFragment { } } - private void updateDependentRatingItems(ContentRatingSystem.Order order, - int selectedRatingOrderIndex, String contentRatingSystemId, boolean isChecked) { + private void updateDependentRatingItems( + ContentRatingSystem.Order order, + int selectedRatingOrderIndex, + String contentRatingSystemId, + boolean isChecked) { List<RatingItem> ratingItems = mContentRatingSystemItemMap.get(contentRatingSystemId); if (ratingItems != null) { for (RatingItem item : ratingItems) { int ratingOrderIndex = item.getRatingOrderIndex(order); if (ratingOrderIndex != -1 && ((ratingOrderIndex > selectedRatingOrderIndex && isChecked) - || (ratingOrderIndex < selectedRatingOrderIndex && !isChecked))) { + || (ratingOrderIndex < selectedRatingOrderIndex && !isChecked))) { item.setRatingBlocked(isChecked); } } @@ -220,9 +221,11 @@ public class RatingsFragment extends SideFragment { private final int mRatingLevel; private RatingLevelItem(int ratingLevel) { - super(getString(sLevelResourceIdMap.get(ratingLevel)), - (sDescriptionResourceIdMap.indexOfKey(ratingLevel) >= 0) ? - getString(sDescriptionResourceIdMap.get(ratingLevel)) : null); + super( + getString(sLevelResourceIdMap.get(ratingLevel)), + (sDescriptionResourceIdMap.indexOfKey(ratingLevel) >= 0) + ? getString(sDescriptionResourceIdMap.get(ratingLevel)) + : null); mRatingLevel = ratingLevel; } @@ -302,8 +305,11 @@ public class RatingsFragment extends SideFragment { } // Automatically check/uncheck dependent ratings. for (int i = 0; i < mOrders.size(); i++) { - updateDependentRatingItems(mOrders.get(i), mOrderIndexes.get(i), - mContentRatingSystem.getId(), isChecked()); + updateDependentRatingItems( + mOrders.get(i), + mOrderIndexes.get(i), + mContentRatingSystem.getId(), + isChecked()); } } @@ -337,14 +343,16 @@ public class RatingsFragment extends SideFragment { @Override protected void onSelected() { - getMainActivity().getOverlayManager().getSideFragmentManager() + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() .show(SubRatingsFragment.create(mContentRatingSystem, mRating.getName())); } @Override protected int getButtonDrawable() { - int blockedStatus = mParentalControlSettings.getBlockedStatus( - mContentRatingSystem, mRating); + int blockedStatus = + mParentalControlSettings.getBlockedStatus(mContentRatingSystem, mRating); if (blockedStatus == ParentalControlSettings.RATING_BLOCKED) { return R.drawable.btn_lock_material; } else if (blockedStatus == ParentalControlSettings.RATING_BLOCKED_PARTIAL) { @@ -354,11 +362,9 @@ public class RatingsFragment extends SideFragment { } } - /** - * Opens a dialog showing the sources of the rating descriptions. - */ + /** Opens a dialog showing the sources of the rating descriptions. */ public static class AttributionItem extends Item { - public final static String DIALOG_TAG = AttributionItem.class.getSimpleName(); + public static final String DIALOG_TAG = AttributionItem.class.getSimpleName(); public static final String TRACKER_LABEL = "Sources for content rating systems"; private final MainActivity mMainActivity; @@ -373,9 +379,11 @@ public class RatingsFragment extends SideFragment { @Override protected void onSelected() { - WebDialogFragment dialog = WebDialogFragment.newInstance( - LicenseUtils.RATING_SOURCE_FILE, - mMainActivity.getString(R.string.option_attribution), TRACKER_LABEL); + WebDialogFragment dialog = + WebDialogFragment.newInstance( + LicenseUtils.RATING_SOURCE_FILE, + mMainActivity.getString(R.string.option_attribution), + TRACKER_LABEL); mMainActivity.getOverlayManager().showDialogFragment(DIALOG_TAG, dialog, false); } } diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java index 4634b74c..b9b03ed1 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java @@ -21,7 +21,6 @@ import android.os.Bundle; import android.view.View; import android.widget.CompoundButton; import android.widget.ImageView; - import com.android.tv.R; import com.android.tv.parental.ContentRatingSystem; import com.android.tv.parental.ContentRatingSystem.Rating; @@ -30,7 +29,6 @@ import com.android.tv.ui.sidepanel.CheckBoxItem; import com.android.tv.ui.sidepanel.DividerItem; import com.android.tv.ui.sidepanel.Item; import com.android.tv.ui.sidepanel.SideFragment; - import java.util.ArrayList; import java.util.List; @@ -44,8 +42,8 @@ public class SubRatingsFragment extends SideFragment { private Rating mRating; private final List<SubRatingItem> mSubRatingItems = new ArrayList<>(); - public static SubRatingsFragment create(ContentRatingSystem contentRatingSystem, - String ratingName) { + public static SubRatingsFragment create( + ContentRatingSystem contentRatingSystem, String ratingName) { SubRatingsFragment fragment = new SubRatingsFragment(); Bundle args = new Bundle(); args.putString(ARGS_CONTENT_RATING_SYSTEM_ID, contentRatingSystem.getId()); @@ -57,8 +55,11 @@ public class SubRatingsFragment extends SideFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mContentRatingSystem = getMainActivity().getContentRatingsManager() - .getContentRatingSystem(getArguments().getString(ARGS_CONTENT_RATING_SYSTEM_ID)); + mContentRatingSystem = + getMainActivity() + .getContentRatingsManager() + .getContentRatingSystem( + getArguments().getString(ARGS_CONTENT_RATING_SYSTEM_ID)); if (mContentRatingSystem != null) { mRating = mContentRatingSystem.getRating(getArguments().getString(ARGS_RATING_NAME)); } @@ -194,22 +195,26 @@ public class SubRatingsFragment extends SideFragment { } private boolean isRatingEnabled() { - return getMainActivity().getParentalControlSettings() + return getMainActivity() + .getParentalControlSettings() .isRatingBlocked(mContentRatingSystem, mRating); } private boolean isSubRatingEnabled(SubRating subRating) { - return getMainActivity().getParentalControlSettings() + return getMainActivity() + .getParentalControlSettings() .isSubRatingEnabled(mContentRatingSystem, mRating, subRating); } private void setRatingEnabled(boolean enabled) { - getMainActivity().getParentalControlSettings() + getMainActivity() + .getParentalControlSettings() .setRatingBlocked(mContentRatingSystem, mRating, enabled); } private void setSubRatingEnabled(SubRating subRating, boolean enabled) { - getMainActivity().getParentalControlSettings() + getMainActivity() + .getParentalControlSettings() .setSubRatingBlocked(mContentRatingSystem, mRating, subRating, enabled); } } diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 477412e4..60fa3018 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -27,24 +27,20 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; - +import com.android.tv.TvSingletons; +import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.data.ChannelImpl; import com.android.tv.data.Program; +import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; - import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Executor; /** * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. * - * <p>Instances of this class should only be executed this using {@link - * #executeOnDbThread(Object[])}. - * * @param <Params> the type of the parameters sent to the task upon execution. * @param <Progress> the type of the progress units published during the background computation. * @param <Result> the type of the result of the background computation. @@ -54,38 +50,19 @@ public abstract class AsyncDbTask<Params, Progress, Result> private static final String TAG = "AsyncDbTask"; private static final boolean DEBUG = false; - private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory( - AsyncDbTask.class.getSimpleName()); - private static final ExecutorService DB_EXECUTOR = Executors - .newSingleThreadExecutor(THREAD_FACTORY); + private final Executor mExecutor; + boolean mCalledExecuteOnDbThread; - /** - * Returns the single tread executor used for DbTasks. - */ - public static ExecutorService getExecutor() { - return DB_EXECUTOR; - } - - /** - * Executes the given command at some time in the future. - * - * <p>The command will be executed by {@link #getExecutor()}. - * - * @param command the runnable task - * @throws RejectedExecutionException if this task cannot be - * accepted for execution - * @throws NullPointerException if command is null - */ - public static void executeOnDbThread(Runnable command) { - DB_EXECUTOR.execute(command); + protected AsyncDbTask(Executor mExecutor) { + this.mExecutor = mExecutor; } /** * Returns the result of a {@link ContentResolver#query(Uri, String[], String, String[], * String)}. * - * <p> {@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)} - * which is implemented by subclasses. + * <p>{@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)} which + * is implemented by subclasses. * * @param <Result> the type of result returned by {@link #onQuery(Cursor)} */ @@ -97,9 +74,15 @@ public abstract class AsyncDbTask<Params, Progress, Result> private final String[] mSelectionArgs; private final String mOrderBy; - - public AsyncQueryTask(ContentResolver contentResolver, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy) { + public AsyncQueryTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy) { + super(executor); mContentResolver = contentResolver; mUri = uri; mProjection = projection; @@ -110,13 +93,15 @@ public abstract class AsyncDbTask<Params, Progress, Result> @Override protected final Result doInBackground(Void... params) { - if (!THREAD_FACTORY.namedWithPrefix(Thread.currentThread())) { - IllegalStateException e = new IllegalStateException(this - + " should only be executed using executeOnDbThread, " - + "but it was called on thread " - + Thread.currentThread()); + if (!mCalledExecuteOnDbThread) { + IllegalStateException e = + new IllegalStateException( + this + + " should only be executed using executeOnDbThread, " + + "but it was called on thread " + + Thread.currentThread()); Log.w(TAG, e); - if (DEBUG) { + if (BuildConfig.ENG) { throw e; } } @@ -128,8 +113,9 @@ public abstract class AsyncDbTask<Params, Progress, Result> if (DEBUG) { Log.v(TAG, "Starting query for " + this); } - try (Cursor c = mContentResolver - .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) { + try (Cursor c = + mContentResolver.query( + mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) { if (c != null && !isCancelled()) { Result result = onQuery(c); if (DEBUG) { @@ -147,7 +133,7 @@ public abstract class AsyncDbTask<Params, Progress, Result> return null; } } catch (Exception e) { - SoftPreconditions.warn(TAG, null, "Error querying " + this, e); + SoftPreconditions.warn(TAG, null, e, "Error querying " + this); return null; } } @@ -176,14 +162,35 @@ public abstract class AsyncDbTask<Params, Progress, Result> public abstract static class AsyncQueryListTask<T> extends AsyncQueryTask<List<T>> { private final CursorFilter mFilter; - public AsyncQueryListTask(ContentResolver contentResolver, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy) { - this(contentResolver, uri, projection, selection, selectionArgs, orderBy, null); + public AsyncQueryListTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy) { + this( + executor, + contentResolver, + uri, + projection, + selection, + selectionArgs, + orderBy, + null); } - public AsyncQueryListTask(ContentResolver contentResolver, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy, CursorFilter filter) { - super(contentResolver, uri, projection, selection, selectionArgs, orderBy); + public AsyncQueryListTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy, + CursorFilter filter) { + super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); mFilter = filter; } @@ -228,9 +235,15 @@ public abstract class AsyncDbTask<Params, Progress, Result> */ public abstract static class AsyncQueryItemTask<T> extends AsyncQueryTask<T> { - public AsyncQueryItemTask(ContentResolver contentResolver, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy) { - super(contentResolver, uri, projection, selection, selectionArgs, orderBy); + public AsyncQueryItemTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy) { + super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy); } @Override @@ -251,7 +264,6 @@ public abstract class AsyncDbTask<Params, Progress, Result> } return null; } - } /** @@ -268,33 +280,55 @@ public abstract class AsyncDbTask<Params, Progress, Result> protected abstract T fromCursor(Cursor c); } - /** - * Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. - */ + /** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */ public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> { - public AsyncChannelQueryTask(ContentResolver contentResolver) { - super(contentResolver, TvContract.Channels.CONTENT_URI, Channel.PROJECTION, - null, null, null); + public AsyncChannelQueryTask(Executor executor, ContentResolver contentResolver) { + super( + executor, + contentResolver, + TvContract.Channels.CONTENT_URI, + ChannelImpl.PROJECTION, + null, + null, + null); } @Override protected final Channel fromCursor(Cursor c) { - return Channel.fromCursor(c); + return ChannelImpl.fromCursor(c); } } - /** - * Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. - */ + /** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */ public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> { - public AsyncProgramQueryTask(ContentResolver contentResolver) { - super(contentResolver, Programs.CONTENT_URI, Program.PROJECTION, null, null, null); + public AsyncProgramQueryTask(Executor executor, ContentResolver contentResolver) { + super( + executor, + contentResolver, + Programs.CONTENT_URI, + Program.PROJECTION, + null, + null, + null); } - public AsyncProgramQueryTask(ContentResolver contentResolver, Uri uri, String selection, - String[] selectionArgs, String sortOrder, CursorFilter filter) { - super(contentResolver, uri, Program.PROJECTION, selection, selectionArgs, sortOrder, + public AsyncProgramQueryTask( + Executor executor, + ContentResolver contentResolver, + Uri uri, + String selection, + String[] selectionArgs, + String sortOrder, + CursorFilter filter) { + super( + executor, + contentResolver, + uri, + Program.PROJECTION, + selection, + selectionArgs, + sortOrder, filter); } @@ -304,13 +338,12 @@ public abstract class AsyncDbTask<Params, Progress, Result> } } - /** - * Gets an {@link List} of {@link TvContract.RecordedPrograms}s. - */ + /** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */ public abstract static class AsyncRecordedProgramQueryTask extends AsyncQueryListTask<RecordedProgram> { - public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) { - super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null); + public AsyncRecordedProgramQueryTask( + Executor executor, ContentResolver contentResolver, Uri uri) { + super(executor, contentResolver, uri, RecordedProgram.PROJECTION, null, null, null); } @Override @@ -319,31 +352,39 @@ public abstract class AsyncDbTask<Params, Progress, Result> } } - /** - * Execute the task on the {@link #DB_EXECUTOR} thread. - */ + /** Execute the task on {@link TvSingletons#getDbExecutor()}. */ @SafeVarargs @MainThread public final void executeOnDbThread(Params... params) { - executeOnExecutor(DB_EXECUTOR, params); + mCalledExecuteOnDbThread = true; + executeOnExecutor(mExecutor, params); } /** * Gets an {@link List} of {@link Program}s for a given channel and period {@link - * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is - * {@code null}, then all the programs is queried. + * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is {@code + * null}, then all the programs is queried. */ public static class LoadProgramsForChannelTask extends AsyncProgramQueryTask { protected final Range<Long> mPeriod; protected final long mChannelId; - public LoadProgramsForChannelTask(ContentResolver contentResolver, long channelId, + public LoadProgramsForChannelTask( + Executor executor, + ContentResolver contentResolver, + long channelId, @Nullable Range<Long> period) { - super(contentResolver, period == null - ? TvContract.buildProgramsUriForChannel(channelId) - : TvContract.buildProgramsUriForChannel(channelId, period.getLower(), - period.getUpper()), - null, null, null, null); + super( + executor, + contentResolver, + period == null + ? TvContract.buildProgramsUriForChannel(channelId) + : TvContract.buildProgramsUriForChannel( + channelId, period.getLower(), period.getUpper()), + null, + null, + null, + null); mPeriod = period; mChannelId = channelId; } @@ -357,14 +398,19 @@ public abstract class AsyncDbTask<Params, Progress, Result> } } - /** - * Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. - */ + /** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */ public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> { - public AsyncQueryProgramTask(ContentResolver contentResolver, long programId) { - super(contentResolver, TvContract.buildProgramUri(programId), Program.PROJECTION, null, - null, null); + public AsyncQueryProgramTask( + Executor executor, ContentResolver contentResolver, long programId) { + super( + executor, + contentResolver, + TvContract.buildProgramUri(programId), + Program.PROJECTION, + null, + null, + null); } @Override @@ -373,8 +419,6 @@ public abstract class AsyncDbTask<Params, Progress, Result> } } - /** - * An interface which filters the row. - */ - public interface CursorFilter extends Filter<Cursor> { } + /** An interface which filters the row. */ + public interface CursorFilter extends Filter<Cursor> {} } diff --git a/src/com/android/tv/util/CaptionSettings.java b/src/com/android/tv/util/CaptionSettings.java index 3b38905b..6d7e9901 100644 --- a/src/com/android/tv/util/CaptionSettings.java +++ b/src/com/android/tv/util/CaptionSettings.java @@ -18,7 +18,6 @@ package com.android.tv.util; import android.content.Context; import android.view.accessibility.CaptioningManager; - import java.util.Locale; public class CaptionSettings { @@ -32,8 +31,8 @@ public class CaptionSettings { private String mTrackId; public CaptionSettings(Context context) { - mCaptioningManager = (CaptioningManager) context.getSystemService( - Context.CAPTIONING_SERVICE); + mCaptioningManager = + (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); } public final String getSystemLanguage() { @@ -84,16 +83,12 @@ public class CaptionSettings { mLanguage = language; } - /** - * Returns the track ID to be used as an alternative key. - */ + /** Returns the track ID to be used as an alternative key. */ public String getTrackId() { return mTrackId; } - /** - * Sets the track ID to be used as an alternative key. - */ + /** Sets the track ID to be used as an alternative key. */ public void setTrackId(String trackId) { mTrackId = trackId; } diff --git a/src/com/android/tv/util/Clock.java b/src/com/android/tv/util/Clock.java deleted file mode 100644 index c5e96431..00000000 --- a/src/com/android/tv/util/Clock.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.tv.util; - -import android.os.SystemClock; - -/** - * An interface through which system clocks can be read. The {@link #SYSTEM} implementation - * must be used for all non-test cases. - */ -public interface Clock { - /** - * Returns the current time in milliseconds since January 1, 1970 00:00:00.0 UTC. - * See {@link System#currentTimeMillis()}. - */ - long currentTimeMillis(); - - /** - * Returns milliseconds since boot, including time spent in sleep. - * - * @see SystemClock#elapsedRealtime() - */ - long elapsedRealtime(); - - /** - * Waits a given number of milliseconds (of uptimeMillis) before returning. - * - * @param ms to sleep before returning, in milliseconds of uptime. - * @see SystemClock#sleep(long) - */ - void sleep(long ms); - - /** - * The default implementation of Clock. - */ - Clock SYSTEM = new Clock() { - @Override - public long currentTimeMillis() { - return System.currentTimeMillis(); - } - - @Override - public long elapsedRealtime() { - return SystemClock.elapsedRealtime(); - } - - @Override - public void sleep(long ms) { - SystemClock.sleep(ms); - } - }; -} diff --git a/src/com/android/tv/util/CompositeComparator.java b/src/com/android/tv/util/CompositeComparator.java index 47cf50fe..ccf4e944 100644 --- a/src/com/android/tv/util/CompositeComparator.java +++ b/src/com/android/tv/util/CompositeComparator.java @@ -18,9 +18,7 @@ package com.android.tv.util; import java.util.Comparator; -/** - * A comparator which runs multiple comparators sequentially. - */ +/** A comparator which runs multiple comparators sequentially. */ public class CompositeComparator<T> implements Comparator<T> { private final Comparator<T>[] mComparators; diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java deleted file mode 100644 index 67a2683d..00000000 --- a/src/com/android/tv/util/Debug.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * A class only for help developers. - */ -public class Debug { - /** - * A threshold of start up time, when the start up time of Live TV is more than it, - * a warning will show to the developer. - */ - public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6); - /** - * Tag for measuring start up time of Live TV. - */ - public static final String TAG_START_UP_TIMER = "start_up_timer"; - - /** - * A global map for duration timers. - */ - private final static Map<String, DurationTimer> sTimerMap = new HashMap<>(); - - /** - * Returns the global duration timer by tag. - */ - public static DurationTimer getTimer(String tag) { - if (sTimerMap.get(tag) != null) { - return sTimerMap.get(tag); - } - DurationTimer timer = new DurationTimer(tag, true); - sTimerMap.put(tag, timer); - return timer; - } - - /** - * Removes the global duration timer by tag. - */ - public static DurationTimer removeTimer(String tag) { - return sTimerMap.remove(tag); - } -} diff --git a/src/com/android/tv/util/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java deleted file mode 100644 index 1f057bf6..00000000 --- a/src/com/android/tv/util/DurationTimer.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.os.SystemClock; -import android.util.Log; - -import com.android.tv.common.BuildConfig; - -/** - * Times a duration. - */ -public final class DurationTimer { - private static final String TAG = "DurationTimer"; - public static final long TIME_NOT_SET = -1; - - private long mStartTimeMs = TIME_NOT_SET; - private String mTag = TAG; - private boolean mLogEngOnly; - - public DurationTimer() { } - - public DurationTimer(String tag, boolean logEngOnly) { - mTag = tag; - mLogEngOnly = logEngOnly; - } - - /** - * Returns true if the timer is running. - */ - public boolean isRunning() { - return mStartTimeMs != TIME_NOT_SET; - } - - /** - * Start the timer. - */ - public void start() { - mStartTimeMs = SystemClock.elapsedRealtime(); - } - - /** - * Returns true if timer is started. - */ - public boolean isStarted() { - return mStartTimeMs != TIME_NOT_SET; - } - - /** - * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long getDuration() { - return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET; - } - - /** - * Stops the timer and resets its value to {@link #TIME_NOT_SET}. - * - * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long reset() { - long duration = getDuration(); - mStartTimeMs = TIME_NOT_SET; - return duration; - } - - /** - * Adds information and duration time to the log. - */ - public void log(String message) { - if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) { - Log.i(mTag, message + " : " + getDuration() + "ms"); - } - } -} diff --git a/src/com/android/tv/util/Filter.java b/src/com/android/tv/util/Filter.java index d5b356e4..3e24a496 100644 --- a/src/com/android/tv/util/Filter.java +++ b/src/com/android/tv/util/Filter.java @@ -16,12 +16,8 @@ package com.android.tv.util; -/** - * Interface to decide whether an input is filtered out or not. - */ +/** Interface to decide whether an input is filtered out or not. */ public interface Filter<T> { - /** - * Returns true, if {@code input} is acceptable. - */ + /** Returns true, if {@code input} is acceptable. */ boolean filter(T input); } diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java deleted file mode 100644 index d5d7bee3..00000000 --- a/src/com/android/tv/util/LocationUtils.java +++ /dev/null @@ -1,143 +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.util; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Address; -import android.location.Geocoder; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.tuner.util.PostalCodeUtils; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; - -/** - * A utility class to get the current location. - */ -public class LocationUtils { - private static final String TAG = "LocationUtils"; - private static final boolean DEBUG = false; - - private static Context sApplicationContext; - private static Address sAddress; - private static String sCountry; - private static IOException sError; - - /** - * Checks the current location. - */ - public static synchronized Address getCurrentAddress(Context context) throws IOException, - SecurityException { - if (sAddress != null) { - return sAddress; - } - if (sError != null) { - throw sError; - } - if (sApplicationContext == null) { - sApplicationContext = context.getApplicationContext(); - } - LocationUtilsHelper.startLocationUpdates(); - return null; - } - - /** Returns the current country. */ - @NonNull - public static synchronized String getCurrentCountry(Context context) { - if (sCountry != null) { - return sCountry; - } - if (TextUtils.isEmpty(sCountry)) { - sCountry = context.getResources().getConfiguration().locale.getCountry(); - } - return sCountry; - } - - private static void updateAddress(Location location) { - if (DEBUG) Log.d(TAG, "Updating address with " + location); - if (location == null) { - return; - } - Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault()); - try { - List<Address> addresses = geocoder.getFromLocation( - location.getLatitude(), location.getLongitude(), 1); - if (addresses != null && !addresses.isEmpty()) { - sAddress = addresses.get(0); - if (DEBUG) Log.d(TAG, "Got " + sAddress); - try { - PostalCodeUtils.updatePostalCode(sApplicationContext); - } catch (Exception e) { - // Do nothing - } - } else { - if (DEBUG) Log.d(TAG, "No address returned"); - } - sError = null; - } catch (IOException e) { - Log.w(TAG, "Error in updating address", e); - sError = e; - } - } - - private LocationUtils() { } - - private static class LocationUtilsHelper { - private static final LocationListener LOCATION_LISTENER = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - updateAddress(location); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { } - - @Override - public void onProviderEnabled(String provider) { } - - @Override - public void onProviderDisabled(String provider) { } - }; - - private static LocationManager sLocationManager; - - public static void startLocationUpdates() { - if (sLocationManager == null) { - sLocationManager = (LocationManager) sApplicationContext.getSystemService( - Context.LOCATION_SERVICE); - try { - sLocationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null); - } catch (SecurityException e) { - // Enables requesting the location updates again. - sLocationManager = null; - throw e; - } - } - } - } -} diff --git a/src/com/android/tv/util/MainThreadExecutor.java b/src/com/android/tv/util/MainThreadExecutor.java index ce8f8ff3..5102ddbd 100644 --- a/src/com/android/tv/util/MainThreadExecutor.java +++ b/src/com/android/tv/util/MainThreadExecutor.java @@ -18,7 +18,6 @@ package com.android.tv.util; import android.os.Handler; import android.os.Looper; - import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.TimeUnit; @@ -26,11 +25,11 @@ import java.util.concurrent.TimeUnit; /** * An executor service that executes its tasks on the main thread. * - * Shutting down this executor is not supported. + * <p>Shutting down this executor is not supported. */ public class MainThreadExecutor extends AbstractExecutorService { - private final static MainThreadExecutor INSTANCE = new MainThreadExecutor(); + private static final MainThreadExecutor INSTANCE = new MainThreadExecutor(); public static MainThreadExecutor getInstance() { return INSTANCE; @@ -47,18 +46,14 @@ public class MainThreadExecutor extends AbstractExecutorService { } } - /** - * Not supported and throws an exception when used. - */ + /** Not supported and throws an exception when used. */ @Override @Deprecated public void shutdown() { throw new UnsupportedOperationException(); } - /** - * Not supported and throws an exception when used. - */ + /** Not supported and throws an exception when used. */ @Override @Deprecated public List<Runnable> shutdownNow() { @@ -75,12 +70,10 @@ public class MainThreadExecutor extends AbstractExecutorService { return false; } - /** - * Not supported and throws an exception when used. - */ + /** Not supported and throws an exception when used. */ @Override @Deprecated public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException { throw new UnsupportedOperationException(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java index 1d5fa80b..a456df91 100644 --- a/src/com/android/tv/util/MultiLongSparseArray.java +++ b/src/com/android/tv/util/MultiLongSparseArray.java @@ -19,7 +19,6 @@ package com.android.tv.util; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; import android.util.LongSparseArray; - import java.util.Collections; import java.util.Set; @@ -29,8 +28,7 @@ import java.util.Set; * <p>This has the same memory and performance trade offs listed in {@link LongSparseArray}. */ public class MultiLongSparseArray<T> { - @VisibleForTesting - static final int DEFAULT_MAX_EMPTIES_KEPT = 4; + @VisibleForTesting static final int DEFAULT_MAX_EMPTIES_KEPT = 4; private final LongSparseArray<Set<T>> mSparseArray; private final Set<T>[] mEmptySets; private int mEmptyIndex = -1; @@ -46,9 +44,8 @@ public class MultiLongSparseArray<T> { } /** - * Adds a mapping from the specified key to the specified value, - * replacing the previous mapping from the specified key if there - * was one. + * Adds a mapping from the specified key to the specified value, replacing the previous mapping + * from the specified key if there was one. */ public void put(long key, T value) { Set<T> values = mSparseArray.get(key); @@ -59,9 +56,7 @@ public class MultiLongSparseArray<T> { values.add(value); } - /** - * Removes the value at the specified index. - */ + /** Removes the value at the specified index. */ public void remove(long key, T value) { Set<T> values = mSparseArray.get(key); if (values != null) { @@ -74,17 +69,15 @@ public class MultiLongSparseArray<T> { } /** - * Gets the set of Objects mapped from the specified key, or an empty set - * if no such mapping has been made. + * Gets the set of Objects mapped from the specified key, or an empty set if no such mapping has + * been made. */ public Iterable<T> get(long key) { Set<T> values = mSparseArray.get(key); return values == null ? Collections.EMPTY_SET : values; } - /** - * Clears cached empty sets. - */ + /** Clears cached empty sets. */ public void clearEmptyCache() { while (mEmptyIndex >= 0) { mEmptySets[mEmptyIndex--] = null; diff --git a/src/com/android/tv/util/NamedThreadFactory.java b/src/com/android/tv/util/NamedThreadFactory.java deleted file mode 100644 index fcdde952..00000000 --- a/src/com/android/tv/util/NamedThreadFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.util; - -import android.support.annotation.NonNull; - -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A thread factory that creates threads with a suffix. - */ -public class NamedThreadFactory implements ThreadFactory { - private final AtomicInteger mCount = new AtomicInteger(0); - private final ThreadFactory mDefaultThreadFactory; - private final String mPrefix; - - public NamedThreadFactory(final String baseName) { - mDefaultThreadFactory = Executors.defaultThreadFactory(); - mPrefix = baseName + "-"; - } - - @Override - public Thread newThread(@NonNull final Runnable runnable) { - final Thread thread = mDefaultThreadFactory.newThread(runnable); - thread.setName(mPrefix + mCount.getAndIncrement()); - return thread; - } - - public boolean namedWithPrefix(Thread thread) { - return thread.getName().startsWith(mPrefix); - } -} diff --git a/src/com/android/tv/util/NetworkTrafficTags.java b/src/com/android/tv/util/NetworkTrafficTags.java deleted file mode 100644 index 2dca613c..00000000 --- a/src/com/android/tv/util/NetworkTrafficTags.java +++ /dev/null @@ -1,64 +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.util; - -import android.net.TrafficStats; -import android.support.annotation.NonNull; - -import java.util.concurrent.Executor; - -/** Constants for tagging network traffic in the Live channels app. */ -public final class NetworkTrafficTags { - - public static final int DEFAULT_LIVE_CHANNELS = 1; - public static final int LOGO_FETCHER = 2; - public static final int HDHOMERUN = 3; - public static final int EPG_FETCH = 4; - - /** - * An executor which simply wraps a provided delegate executor, but calls {@link - * TrafficStats#setThreadStatsTag(int)} before executing any task. - */ - public static class TrafficStatsTaggingExecutor implements Executor { - private final Executor delegateExecutor; - private final int tag; - - public TrafficStatsTaggingExecutor(Executor delegateExecutor, int tag) { - this.delegateExecutor = delegateExecutor; - this.tag = tag; - } - - @Override - public void execute(final @NonNull Runnable command) { - // TODO(b/62038127): robolectric does not support lamdas in unbundled apps - delegateExecutor.execute( - new Runnable() { - @Override - public void run() { - TrafficStats.setThreadStatsTag(tag); - try { - command.run(); - } finally { - TrafficStats.clearThreadStatsTag(); - } - } - }); - } - } - - private NetworkTrafficTags() {} -} diff --git a/src/com/android/tv/util/NetworkUtils.java b/src/com/android/tv/util/NetworkUtils.java index ed3ce383..94581d5a 100644 --- a/src/com/android/tv/util/NetworkUtils.java +++ b/src/com/android/tv/util/NetworkUtils.java @@ -20,21 +20,16 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; - import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -/** - * A utility class to check the connectivity. - */ +/** A utility class to check the connectivity. */ @WorkerThread public class NetworkUtils { private static final String GENERATE_204 = "http://clients3.google.com/generate_204"; - /** - * Checks if the internet connection is available. - */ + /** Checks if the internet connection is available. */ public static boolean isNetworkAvailable(@Nullable ConnectivityManager connectivityManager) { if (connectivityManager == null) { return false; @@ -62,5 +57,5 @@ public class NetworkUtils { return false; } - private NetworkUtils() { } + private NetworkUtils() {} } diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java index 49b02b82..3b72e091 100644 --- a/src/com/android/tv/util/OnboardingUtils.java +++ b/src/com/android/tv/util/OnboardingUtils.java @@ -21,9 +21,7 @@ import android.content.Intent; import android.net.Uri; import android.preference.PreferenceManager; -/** - * A utility class related to onboarding experience. - */ +/** A utility class related to onboarding experience. */ public final class OnboardingUtils { private static final String PREF_KEY_IS_FIRST_BOOT = "pref_onbaording_is_first_boot"; private static final String PREF_KEY_ONBOARDING_VERSION_CODE = "pref_onbaording_versionCode"; @@ -31,23 +29,17 @@ public final class OnboardingUtils { private static final String MERCHANT_COLLECTION_URL_STRING = getMerchantCollectionUrl(); - /** - * Intent to show merchant collection in online store. - */ - public static final Intent ONLINE_STORE_INTENT = new Intent(Intent.ACTION_VIEW, - Uri.parse(MERCHANT_COLLECTION_URL_STRING)); + /** Intent to show merchant collection in online store. */ + public static final Intent ONLINE_STORE_INTENT = + new Intent(Intent.ACTION_VIEW, Uri.parse(MERCHANT_COLLECTION_URL_STRING)); - /** - * Checks if this is the first boot after the onboarding experience has been applied. - */ + /** Checks if this is the first boot after the onboarding experience has been applied. */ public static boolean isFirstBoot(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(PREF_KEY_IS_FIRST_BOOT, true); } - /** - * Marks that the first boot has been completed. - */ + /** Marks that the first boot has been completed. */ public static void setFirstBootCompleted(Context context) { PreferenceManager.getDefaultSharedPreferences(context) .edit() @@ -56,27 +48,28 @@ public final class OnboardingUtils { } /** - * Checks if this is the first run of {@link com.android.tv.MainActivity} with the - * current onboarding version. + * Checks if this is the first run of {@link com.android.tv.MainActivity} with the current + * onboarding version. */ public static boolean isFirstRunWithCurrentVersion(Context context) { - int versionCode = PreferenceManager.getDefaultSharedPreferences(context) - .getInt(PREF_KEY_ONBOARDING_VERSION_CODE, 0); + int versionCode = + PreferenceManager.getDefaultSharedPreferences(context) + .getInt(PREF_KEY_ONBOARDING_VERSION_CODE, 0); return versionCode != ONBOARDING_VERSION; } /** - * Marks that the first run of {@link com.android.tv.MainActivity} with the current - * onboarding version has been completed. + * Marks that the first run of {@link com.android.tv.MainActivity} with the current onboarding + * version has been completed. */ public static void setFirstRunWithCurrentVersionCompleted(Context context) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putInt(PREF_KEY_ONBOARDING_VERSION_CODE, ONBOARDING_VERSION).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(PREF_KEY_ONBOARDING_VERSION_CODE, ONBOARDING_VERSION) + .apply(); } - /** - * Returns merchant collection URL. - */ + /** Returns merchant collection URL. */ private static String getMerchantCollectionUrl() { return "TODO: add a merchant collection url"; } diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java index e3688392..c5e9aad2 100644 --- a/src/com/android/tv/util/Partner.java +++ b/src/com/android/tv/util/Partner.java @@ -26,7 +26,6 @@ import android.content.res.Resources; import android.media.tv.TvInputInfo; import android.text.TextUtils; import android.util.Log; - import java.util.HashMap; import java.util.Map; @@ -42,6 +41,7 @@ public class Partner { /** ID tags for device input types */ public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners"; + public static final String INPUT_TYPE_TUNER = "input_type_tuner"; public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical"; public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder"; @@ -68,6 +68,7 @@ public class Partner { private final Resources mResources; private static final Map<String, Integer> INPUT_TYPE_MAP = new HashMap<>(); + static { INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER); INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER); diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java deleted file mode 100644 index a355be99..00000000 --- a/src/com/android/tv/util/PermissionUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.android.tv.util; - -import android.content.Context; -import android.content.pm.PackageManager; - -/** - * Util class to handle permissions. - */ -public class PermissionUtils { - /** - * Permission to read the TV listings. - */ - public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; - - private static Boolean sHasAccessAllEpgPermission; - private static Boolean sHasAccessWatchedHistoryPermission; - private static Boolean sHasModifyParentalControlsPermission; - - public static boolean hasAccessAllEpg(Context context) { - if (sHasAccessAllEpgPermission == null) { - sHasAccessAllEpgPermission = context.checkSelfPermission( - "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA") - == PackageManager.PERMISSION_GRANTED; - } - return sHasAccessAllEpgPermission; - } - - public static boolean hasAccessWatchedHistory(Context context) { - if (sHasAccessWatchedHistoryPermission == null) { - sHasAccessWatchedHistoryPermission = context.checkSelfPermission( - "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS") - == PackageManager.PERMISSION_GRANTED; - } - return sHasAccessWatchedHistoryPermission; - } - - public static boolean hasModifyParentalControls(Context context) { - if (sHasModifyParentalControlsPermission == null) { - sHasModifyParentalControlsPermission = context.checkSelfPermission( - "android.permission.MODIFY_PARENTAL_CONTROLS") - == PackageManager.PERMISSION_GRANTED; - } - return sHasModifyParentalControlsPermission; - } - - public static boolean hasReadTvListings(Context context) { - return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS) - == PackageManager.PERMISSION_GRANTED; - } - - public static boolean hasInternet(Context context) { - return context.checkSelfPermission("android.permission.INTERNET") - == PackageManager.PERMISSION_GRANTED; - } -} diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 8b45131b..764689c2 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -22,10 +22,8 @@ import android.os.AsyncTask; import android.os.Handler; import android.support.annotation.WorkerThread; import android.util.Log; - -import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.common.SoftPreconditions; - +import com.android.tv.common.util.SharedPreferencesUtils; import java.util.Date; /** @@ -46,8 +44,8 @@ public final class RecurringRunner { private final String mName; private boolean mRunning; - public RecurringRunner(Context context, long intervalMs, Runnable runnable, - Runnable onStopRunnable) { + public RecurringRunner( + Context context, long intervalMs, Runnable runnable, Runnable onStopRunnable) { mContext = context.getApplicationContext(); mRunnable = runnable; mOnStopRunnable = onStopRunnable; @@ -99,18 +97,21 @@ public final class RecurringRunner { // Run it anyways even if it is in the past if (DEBUG) Log.i(TAG, "Next run of " + mName + " at " + new Date(next)); long delay = Math.max(next - now, 0); - boolean posted = mHandler.postDelayed(new Runnable() { - @Override - public void run() { - try { - if (DEBUG) Log.i(TAG, "Starting " + mName); - mRunnable.run(); - } catch (Exception e) { - Log.w(TAG, "Error running " + mName, e); - } - postAt(resetNextRunTime()); - } - }, delay); + boolean posted = + mHandler.postDelayed( + new Runnable() { + @Override + public void run() { + try { + if (DEBUG) Log.i(TAG, "Starting " + mName); + mRunnable.run(); + } catch (Exception e) { + Log.w(TAG, "Error running " + mName, e); + } + postAt(resetNextRunTime()); + } + }, + delay); if (!posted) { Log.w(TAG, "Scheduling a future run of " + mName + " at " + new Date(next) + "failed"); } @@ -118,8 +119,8 @@ public final class RecurringRunner { } private SharedPreferences getSharedPreferences() { - return mContext.getSharedPreferences(SharedPreferencesUtils.SHARED_PREF_RECURRING_RUNNER, - Context.MODE_PRIVATE); + return mContext.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_RECURRING_RUNNER, Context.MODE_PRIVATE); } @WorkerThread diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 32e3a81f..0d536320 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -28,24 +28,20 @@ import android.media.tv.TvInputManager; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; +import com.android.tv.TvSingletons; +import com.android.tv.common.BaseApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.tuner.tvinput.TunerTvInputService; - +import com.android.tv.data.api.Channel; import java.util.Collections; import java.util.HashSet; import java.util.Set; -/** - * A utility class related to input setup. - */ +/** A utility class related to input setup. */ public class SetupUtils { private static final String TAG = "SetupUtils"; private static final boolean DEBUG = false; @@ -58,9 +54,8 @@ public class SetupUtils { // Recognized inputs means that the user already knows the inputs are installed. private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs"; private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune"; - private static SetupUtils sSetupUtils; - private final TvApplication mTvApplication; + private final Context mContext; private final SharedPreferences mSharedPreferences; private final Set<String> mKnownInputs; private final Set<String> mSetUpInputs; @@ -68,106 +63,104 @@ public class SetupUtils { private boolean mIsFirstTune; private final String mTunerInputId; - private SetupUtils(TvApplication tvApplication) { - mTvApplication = tvApplication; - mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication); + @VisibleForTesting + protected SetupUtils(Context context) { + mContext = context; + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); mSetUpInputs = new ArraySet<>(); - mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, - Collections.emptySet())); + mSetUpInputs.addAll( + mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet())); mKnownInputs = new ArraySet<>(); - mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, - Collections.emptySet())); + mKnownInputs.addAll( + mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet())); mRecognizedInputs = new ArraySet<>(); - mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, - mKnownInputs)); + mRecognizedInputs.addAll( + mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); - mTunerInputId = TvContract.buildInputId(new ComponentName(tvApplication, - TunerTvInputService.class)); + mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); } /** - * Gets an instance of {@link SetupUtils}. + * Creates an instance of {@link SetupUtils}. + * + * <p><b>WARNING</b> this should only be called by the top level application. */ - public static SetupUtils getInstance(Context context) { - if (sSetupUtils != null) { - return sSetupUtils; - } - sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext()); - return sSetupUtils; + public static SetupUtils createForTvSingletons(Context context) { + return new SetupUtils(context.getApplicationContext()); } - /** - * Additional work after the setup of TV input. - */ - public void onTvInputSetupFinished(final String inputId, - @Nullable final Runnable postRunnable) { + /** Additional work after the setup of TV input. */ + public void onTvInputSetupFinished( + final String inputId, @Nullable final Runnable postRunnable) { // When TIS adds several channels, ChannelDataManager.Listener.onChannelList // Updated() can be called several times. In this case, it is hard to detect // which one is the last callback. To reduce error prune, we update channel // list again and make all channels of {@code inputId} browsable. onSetupDone(inputId); - final ChannelDataManager manager = mTvApplication.getChannelDataManager(); + final ChannelDataManager manager = + TvSingletons.getSingletons(mContext).getChannelDataManager(); if (!manager.isDbLoadFinished()) { - manager.addListener(new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - manager.removeListener(this); - updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); - } + manager.addListener( + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + manager.removeListener(this); + updateChannelsAfterSetup(mContext, inputId, postRunnable); + } - @Override - public void onChannelListUpdated() { } + @Override + public void onChannelListUpdated() {} - @Override - public void onChannelBrowsableChanged() { } - }); + @Override + public void onChannelBrowsableChanged() {} + }); } else { - updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); + updateChannelsAfterSetup(mContext, inputId, postRunnable); } } - private static void updateChannelsAfterSetup(Context context, final String inputId, - final Runnable postRunnable) { - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - final ChannelDataManager manager = appSingletons.getChannelDataManager(); - manager.updateChannels(new Runnable() { - @Override - public void run() { - Channel firstChannelForInput = null; - boolean browsableChanged = false; - for (Channel channel : manager.getChannelList()) { - if (channel.getInputId().equals(inputId)) { - if (!channel.isBrowsable()) { - manager.updateBrowsable(channel.getId(), true, true); - browsableChanged = true; + private static void updateChannelsAfterSetup( + Context context, final String inputId, final Runnable postRunnable) { + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + final ChannelDataManager manager = tvSingletons.getChannelDataManager(); + manager.updateChannels( + new Runnable() { + @Override + public void run() { + Channel firstChannelForInput = null; + boolean browsableChanged = false; + for (Channel channel : manager.getChannelList()) { + if (channel.getInputId().equals(inputId)) { + if (!channel.isBrowsable()) { + manager.updateBrowsable(channel.getId(), true, true); + browsableChanged = true; + } + if (firstChannelForInput == null) { + firstChannelForInput = channel; + } + } + } + if (firstChannelForInput != null) { + Utils.setLastWatchedChannel(context, firstChannelForInput); } - if (firstChannelForInput == null) { - firstChannelForInput = channel; + if (browsableChanged) { + manager.notifyChannelBrowsableChanged(); + manager.applyUpdatedValuesToDb(); + } + if (postRunnable != null) { + postRunnable.run(); } } - } - if (firstChannelForInput != null) { - Utils.setLastWatchedChannel(context, firstChannelForInput); - } - if (browsableChanged) { - manager.notifyChannelBrowsableChanged(); - manager.applyUpdatedValuesToDb(); - } - if (postRunnable != null) { - postRunnable.run(); - } - } - }); + }); } - /** - * Marks the channels in newly installed inputs browsable. - */ + /** Marks the channels in newly installed inputs browsable. */ @UiThread public void markNewChannelsBrowsable() { Set<String> newInputsWithChannels = new HashSet<>(); - TvInputManagerHelper tvInputManagerHelper = mTvApplication.getTvInputManagerHelper(); - ChannelDataManager channelDataManager = mTvApplication.getChannelDataManager(); + TvSingletons singletons = TvSingletons.getSingletons(mContext); + TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper(); + ChannelDataManager channelDataManager = singletons.getChannelDataManager(); SoftPreconditions.checkState(channelDataManager.isDbLoadFinished()); for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) { String inputId = input.getId(); @@ -175,9 +168,13 @@ public class SetupUtils { onSetupDone(inputId); newInputsWithChannels.add(inputId); if (DEBUG) { - Log.d(TAG, "New input " + inputId + " has " - + channelDataManager.getChannelCountForInput(inputId) - + " channels"); + Log.d( + TAG, + "New input " + + inputId + + " has " + + channelDataManager.getChannelCountForInput(inputId) + + " channels"); } } } @@ -195,9 +192,7 @@ public class SetupUtils { return mIsFirstTune; } - /** - * Returns true, if the input with {@code inputId} is newly installed. - */ + /** Returns true, if the input with {@code inputId} is newly installed. */ public boolean isNewInput(String inputId) { return !mKnownInputs.contains(inputId); } @@ -209,13 +204,14 @@ public class SetupUtils { public void markAsKnownInput(String inputId) { mKnownInputs.add(inputId); mRecognizedInputs.add(inputId); - mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) - .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); + mSharedPreferences + .edit() + .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) + .apply(); } - /** - * Returns {@code true}, if {@code inputId}'s setup has been done before. - */ + /** Returns {@code true}, if {@code inputId}'s setup has been done before. */ public boolean isSetupDone(String inputId) { boolean done = mSetUpInputs.contains(inputId); if (DEBUG) { @@ -224,9 +220,7 @@ public class SetupUtils { return done; } - /** - * Returns true, if there is any newly installed input. - */ + /** Returns true, if there is any newly installed input. */ public boolean hasNewInput(TvInputManagerHelper inputManager) { for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { if (isNewInput(input.getId())) { @@ -236,9 +230,7 @@ public class SetupUtils { return false; } - /** - * Checks whether the given input is already recognized by the user or not. - */ + /** Checks whether the given input is already recognized by the user or not. */ private boolean isRecognizedInput(String inputId) { return mRecognizedInputs.contains(inputId); } @@ -251,13 +243,13 @@ public class SetupUtils { for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { mRecognizedInputs.add(input.getId()); } - mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) + mSharedPreferences + .edit() + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) .apply(); } - /** - * Checks whether there are any unrecognized inputs. - */ + /** Checks whether there are any unrecognized inputs. */ public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) { for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { if (!isRecognizedInput(input.getId())) { @@ -276,8 +268,8 @@ public class SetupUtils { // Find all already-verified packages. Set<String> setUpPackages = new HashSet<>(); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, - Collections.<String>emptySet())) { + for (String input : + sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) { if (!TextUtils.isEmpty(input)) { ComponentName componentName = ComponentName.unflattenFromString(input); if (componentName != null) { @@ -299,23 +291,28 @@ public class SetupUtils { */ public static void grantEpgPermission(Context context, String packageName) { if (DEBUG) { - Log.d(TAG, "grantEpgPermission(context=" + context + ", packageName=" + packageName - + ")"); + Log.d( + TAG, + "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")"); } try { - int modeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; + int modeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags); context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags); } catch (SecurityException e) { - Log.e(TAG, "Either TvProvider does not allow granting of Uri permissions or the app" - + " does not have permission.", e); + Log.e( + TAG, + "Either TvProvider does not allow granting of Uri permissions or the app" + + " does not have permission.", + e); } } /** - * Called when Live channels app is launched. Once it is called, {@link - * #isFirstTune} will return false. + * Called when Live channels app is launched. Once it is called, {@link #isFirstTune} will + * return false. */ public void onTuned() { if (!mIsFirstTune) { @@ -325,9 +322,7 @@ public class SetupUtils { mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply(); } - /** - * Called when input list is changed. It mainly handles input removals. - */ + /** Called when input list is changed. It mainly handles input removals. */ public void onInputListUpdated(TvInputManager manager) { // mRecognizedInputs > mKnownInputs > mSetUpInputs. Set<String> removedInputList = new HashSet<>(mRecognizedInputs); @@ -345,9 +340,10 @@ public class SetupUtils { try { // Just after booting, input list from TvInputManager are not reliable. // So we need to double-check package existence. b/29034900 - mTvApplication.getPackageManager().getPackageInfo( - ComponentName.unflattenFromString(input) - .getPackageName(), PackageManager.GET_ACTIVITIES); + mContext.getPackageManager() + .getPackageInfo( + ComponentName.unflattenFromString(input).getPackageName(), + PackageManager.GET_ACTIVITIES); Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); } catch (NameNotFoundException e) { Log.i(TAG, "TV input (" + input + ") and its package are removed"); @@ -358,9 +354,12 @@ public class SetupUtils { } } if (inputPackageDeleted) { - mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) + mSharedPreferences + .edit() + .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) - .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) + .apply(); } } } @@ -375,7 +374,9 @@ public class SetupUtils { if (!mRecognizedInputs.contains(inputId)) { Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); mRecognizedInputs.add(inputId); - mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) + mSharedPreferences + .edit() + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) .apply(); } if (!mKnownInputs.contains(inputId)) { @@ -388,4 +389,4 @@ public class SetupUtils { mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java new file mode 100644 index 00000000..c4b803b6 --- /dev/null +++ b/src/com/android/tv/util/SqlParams.java @@ -0,0 +1,74 @@ +/* + * 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.util; + +import android.database.DatabaseUtils; +import java.util.Arrays; + +/** Convenience class for SQL operations. */ +public class SqlParams { + private String mTables; + private String mSelection; + private String[] mSelectionArgs; + + public SqlParams(String tables, String selection, String... selectionArgs) { + setTables(tables); + setWhere(selection, selectionArgs); + } + + public String getTables() { + return mTables; + } + + public String getSelection() { + return mSelection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setTables(String tables) { + mTables = tables; + } + + public void setWhere(String selection, String... selectionArgs) { + mSelection = selection; + mSelectionArgs = selectionArgs; + } + + public void appendWhere(String selection, String... selectionArgs) { + mSelection = DatabaseUtils.concatenateWhere(mSelection, selection); + if (selectionArgs != null) { + mSelectionArgs = DatabaseUtils.appendSelectionArgs(mSelectionArgs, selectionArgs); + } + } + + public void appendWhereEquals(String name, String value) { + appendWhere(name + "=?", value); + } + + @Override + public String toString() { + return "tables " + + getTables() + + " where " + + getSelection() + + " with " + + Arrays.toString(getSelectionArgs()); + } +} diff --git a/src/com/android/tv/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java deleted file mode 100644 index 659807e2..00000000 --- a/src/com/android/tv/util/StringUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -/** - * Utility class for handling {@link String}. - */ -public final class StringUtils { - - private StringUtils() { } - - /** - * Returns compares two strings lexicographically and handles null values quietly. - */ - public static int compare(String a, String b) { - if (a == null) { - return b == null ? 0 : -1; - } - if (b == null) { - return 1; - } - return a.compareTo(b); - } -} diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java deleted file mode 100644 index e737f233..00000000 --- a/src/com/android/tv/util/SystemProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import com.android.tv.common.BooleanSystemProperty; - -/** - * A convenience class for getting TV related system properties. - */ -public final class SystemProperties { - - /** - * Allow Google Analytics for eng builds. - */ - public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG = new BooleanSystemProperty( - "tv_allow_analytics_in_eng", false); - - /** - * Allow Strict mode for debug builds. - */ - public static final BooleanSystemProperty ALLOW_STRICT_MODE = new BooleanSystemProperty( - "tv_allow_strict_mode", true); - - /** - * When true {@link android.view.KeyEvent}s are logged. Defaults to false. - */ - public static final BooleanSystemProperty LOG_KEYEVENT = new BooleanSystemProperty( - "tv_log_keyevent", false); - /** - * When true debug keys are used. Defaults to false. - */ - public static final BooleanSystemProperty USE_DEBUG_KEYS = new BooleanSystemProperty( - "tv_use_debug_keys", false); - - /** - * Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. - */ - public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty( - "tv_use_tracker", true); - - static { - updateSystemProperties(); - } - - private SystemProperties() { - } - - /** - * Update the TV related system properties. - */ - public static void updateSystemProperties() { - BooleanSystemProperty.resetAll(); - } -} diff --git a/src/com/android/tv/util/TimeShiftUtils.java b/src/com/android/tv/util/TimeShiftUtils.java index 8038a78f..977f3333 100644 --- a/src/com/android/tv/util/TimeShiftUtils.java +++ b/src/com/android/tv/util/TimeShiftUtils.java @@ -18,9 +18,7 @@ package com.android.tv.util; import java.util.concurrent.TimeUnit; -/** - * A class that includes convenience methods for time shift plays. - */ +/** A class that includes convenience methods for time shift plays. */ public class TimeShiftUtils { private static final String TAG = "TimeShiftUtils"; private static final boolean DEBUG = false; @@ -30,8 +28,8 @@ public class TimeShiftUtils { private static final int[] LONG_PROGRAM_SPEED_FACTORS = new int[] {2, 8, 32, 128}; /** - * The maximum play speed level support by time shift play. In other words, the valid - * speed levels are ranged from 0 to MAX_SPEED_LEVEL (included). + * The maximum play speed level support by time shift play. In other words, the valid speed + * levels are ranged from 0 to MAX_SPEED_LEVEL (included). */ public static final int MAX_SPEED_LEVEL = SHORT_PROGRAM_SPEED_FACTORS.length - 1; @@ -45,17 +43,19 @@ public class TimeShiftUtils { */ public static int getPlaybackSpeed(int speedLevel, long programDurationMillis) throws IndexOutOfBoundsException { - return (programDurationMillis > SHORT_PROGRAM_THRESHOLD_MILLIS) ? - LONG_PROGRAM_SPEED_FACTORS[speedLevel] : SHORT_PROGRAM_SPEED_FACTORS[speedLevel]; + return (programDurationMillis > SHORT_PROGRAM_THRESHOLD_MILLIS) + ? LONG_PROGRAM_SPEED_FACTORS[speedLevel] + : SHORT_PROGRAM_SPEED_FACTORS[speedLevel]; } /** * Returns the maxium possible play speed according to the program's length. + * * @param programDurationMillis the length of program under playing. */ public static int getMaxPlaybackSpeed(long programDurationMillis) { - return (programDurationMillis > SHORT_PROGRAM_THRESHOLD_MILLIS) ? - LONG_PROGRAM_SPEED_FACTORS[MAX_SPEED_LEVEL] + return (programDurationMillis > SHORT_PROGRAM_THRESHOLD_MILLIS) + ? LONG_PROGRAM_SPEED_FACTORS[MAX_SPEED_LEVEL] : SHORT_PROGRAM_SPEED_FACTORS[MAX_SPEED_LEVEL]; } } diff --git a/src/com/android/tv/util/ToastUtils.java b/src/com/android/tv/util/ToastUtils.java index 34346b2a..a25653f6 100644 --- a/src/com/android/tv/util/ToastUtils.java +++ b/src/com/android/tv/util/ToastUtils.java @@ -19,18 +19,13 @@ package com.android.tv.util; import android.content.Context; import android.support.annotation.MainThread; import android.widget.Toast; - import java.lang.ref.WeakReference; -/** - * A utility class for the toast message. - */ +/** A utility class for the toast message. */ public class ToastUtils { private static WeakReference<Toast> sToast; - /** - * Shows the toast message after canceling the previous one. - */ + /** Shows the toast message after canceling the previous one. */ @MainThread public static void show(Context context, CharSequence text, int duration) { if (sToast != null && sToast.get() != null) { diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 730a985b..625fb7b2 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -21,21 +21,23 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.tv.TvContentRatingSystemInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; - -import com.android.tv.Features; +import com.android.tv.TvFeatures; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvCommonUtils; +import com.android.tv.common.util.CommonUtils; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; - +import com.android.tv.util.images.ImageCache; +import com.android.tv.util.images.ImageLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -49,10 +51,61 @@ public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; - /** - * Types of HDMI device and bundled tuner. - */ + public interface TvInputManagerInterface { + TvInputInfo getTvInputInfo(String inputId); + + Integer getInputState(String inputId); + + void registerCallback(TvInputCallback internalCallback, Handler handler); + + void unregisterCallback(TvInputCallback internalCallback); + + List<TvInputInfo> getTvInputList(); + + List<TvContentRatingSystemInfo> getTvContentRatingSystemList(); + } + + private static final class TvInputManagerImpl implements TvInputManagerInterface { + private final TvInputManager delegate; + + private TvInputManagerImpl(TvInputManager delegate) { + this.delegate = delegate; + } + + @Override + public TvInputInfo getTvInputInfo(String inputId) { + return delegate.getTvInputInfo(inputId); + } + + @Override + public Integer getInputState(String inputId) { + return delegate.getInputState(inputId); + } + + @Override + public void registerCallback(TvInputCallback internalCallback, Handler handler) { + delegate.registerCallback(internalCallback, handler); + } + + @Override + public void unregisterCallback(TvInputCallback internalCallback) { + delegate.unregisterCallback(internalCallback); + } + + @Override + public List<TvInputInfo> getTvInputList() { + return delegate.getTvInputList(); + } + + @Override + public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() { + return delegate.getTvContentRatingSystemList(); + } + } + + /** Types of HDMI device and bundled tuner. */ public static final int TYPE_CEC_DEVICE = -2; + public static final int TYPE_BUNDLED_TUNER = -3; public static final int TYPE_CEC_DEVICE_RECORDER = -4; public static final int TYPE_CEC_DEVICE_PLAYBACK = -5; @@ -60,14 +113,13 @@ public class TvInputManagerHelper { private static final String PERMISSION_ACCESS_ALL_EPG_DATA = "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; - private static final String [] mPhysicalTunerBlackList = { + private static final String[] mPhysicalTunerBlackList = { }; private static final String META_LABEL_SORT_KEY = "input_sort_key"; - /** - * The default tv input priority to show. - */ + /** The default tv input priority to show. */ private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>(); + static { DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER); DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER); @@ -90,12 +142,12 @@ public class TvInputManagerHelper { }; private static final String[] TESTABLE_INPUTS = { - "com.android.tv.testinput/.TestTvInputService" + "com.android.tv.testinput/.TestTvInputService" }; private final Context mContext; private final PackageManager mPackageManager; - private final TvInputManager mTvInputManager; + protected final TvInputManagerInterface mTvInputManager; private final Map<String, Integer> mInputStateMap = new HashMap<>(); private final Map<String, TvInputInfo> mInputMap = new HashMap<>(); private final Map<String, String> mTvInputLabels = new ArrayMap<>(); @@ -106,100 +158,105 @@ public class TvInputManagerHelper { private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>(); private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>(); - private final TvInputCallback mInternalCallback = new TvInputCallback() { - @Override - public void onInputStateChanged(String inputId, int state) { - if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state); - if (isInBlackList(inputId)) { - return; - } - mInputStateMap.put(inputId, state); - for (TvInputCallback callback : mCallbacks) { - callback.onInputStateChanged(inputId, state); - } - } + private final TvInputCallback mInternalCallback = + new TvInputCallback() { + @Override + public void onInputStateChanged(String inputId, int state) { + if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state); + if (isInBlackList(inputId)) { + return; + } + mInputStateMap.put(inputId, state); + for (TvInputCallback callback : mCallbacks) { + callback.onInputStateChanged(inputId, state); + } + } - @Override - public void onInputAdded(String inputId) { - if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); - if (isInBlackList(inputId)) { - return; - } - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); - if (info != null) { - mInputMap.put(inputId, info); - mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = info.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + @Override + public void onInputAdded(String inputId) { + if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); + if (isInBlackList(inputId)) { + return; + } + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + if (info != null) { + mInputMap.put(inputId, info); + CharSequence label = info.loadLabel(mContext); + // in tests the label may be missing just use the input id + mTvInputLabels.put(inputId, label != null ? label.toString() : inputId); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } + mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); + mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); + } + mContentRatingsManager.update(); + for (TvInputCallback callback : mCallbacks) { + callback.onInputAdded(inputId); + } } - mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); - mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); - } - mContentRatingsManager.update(); - for (TvInputCallback callback : mCallbacks) { - callback.onInputAdded(inputId); - } - } - @Override - public void onInputRemoved(String inputId) { - if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); - mInputMap.remove(inputId); - mTvInputLabels.remove(inputId); - mTvInputCustomLabels.remove(inputId); - mTvInputApplicationLabels.remove(inputId); - mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); - mInputStateMap.remove(inputId); - mInputIdToPartnerInputMap.remove(inputId); - mContentRatingsManager.update(); - for (TvInputCallback callback : mCallbacks) { - callback.onInputRemoved(inputId); - } - ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( - inputId)); - } + @Override + public void onInputRemoved(String inputId) { + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); + mInputMap.remove(inputId); + mTvInputLabels.remove(inputId); + mTvInputCustomLabels.remove(inputId); + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); + mInputStateMap.remove(inputId); + mInputIdToPartnerInputMap.remove(inputId); + mContentRatingsManager.update(); + for (TvInputCallback callback : mCallbacks) { + callback.onInputRemoved(inputId); + } + ImageCache.getInstance() + .remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey(inputId)); + } - @Override - public void onInputUpdated(String inputId) { - if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId); - if (isInBlackList(inputId)) { - return; - } - TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); - mInputMap.put(inputId, info); - mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = info.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); - } - mTvInputApplicationLabels.remove(inputId); - mTvInputApplicationIcons.remove(inputId); - mTvInputAppliactionBanners.remove(inputId); - for (TvInputCallback callback : mCallbacks) { - callback.onInputUpdated(inputId); - } - ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( - inputId)); - } + @Override + public void onInputUpdated(String inputId) { + if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId); + if (isInBlackList(inputId)) { + return; + } + TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); + mInputMap.put(inputId, info); + mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); + for (TvInputCallback callback : mCallbacks) { + callback.onInputUpdated(inputId); + } + ImageCache.getInstance() + .remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey(inputId)); + } - @Override - public void onTvInputInfoUpdated(TvInputInfo inputInfo) { - if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); - mInputMap.put(inputInfo.getId(), inputInfo); - mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); - CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); - if (inputCustomLabel != null) { - mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString()); - } - for (TvInputCallback callback : mCallbacks) { - callback.onTvInputInfoUpdated(inputInfo); - } - ImageCache.getInstance().remove(ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( - inputInfo.getId())); - } - }; + @Override + public void onTvInputInfoUpdated(TvInputInfo inputInfo) { + if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); + mInputMap.put(inputInfo.getId(), inputInfo); + mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString()); + } + for (TvInputCallback callback : mCallbacks) { + callback.onTvInputInfoUpdated(inputInfo); + } + ImageCache.getInstance() + .remove( + ImageLoader.LoadTvInputLogoTask.getTvInputLogoKey( + inputInfo.getId())); + } + }; private final Handler mHandler = new Handler(); private boolean mStarted; @@ -209,10 +266,23 @@ public class TvInputManagerHelper { private final Comparator<TvInputInfo> mTvInputInfoComparator; public TvInputManagerHelper(Context context) { + this(context, createTvInputManagerWrapper(context)); + } + + @Nullable + protected static TvInputManagerImpl createTvInputManagerWrapper(Context context) { + TvInputManager tvInputManager = + (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); + return tvInputManager == null ? null : new TvInputManagerImpl(tvInputManager); + } + + @VisibleForTesting + protected TvInputManagerHelper( + Context context, @Nullable TvInputManagerInterface tvInputManager) { mContext = context.getApplicationContext(); mPackageManager = context.getPackageManager(); - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - mContentRatingsManager = new ContentRatingsManager(context); + mTvInputManager = tvInputManager; + mContentRatingsManager = new ContentRatingsManager(context, tvInputManager); mParentalControlSettings = new ParentalControlSettings(context); mTvInputInfoComparator = new InputComparatorInternal(this); } @@ -247,7 +317,9 @@ public class TvInputManagerHelper { mInputStateMap.put(inputId, state); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input)); } - SoftPreconditions.checkState(mInputStateMap.size() == mInputMap.size(), TAG, + SoftPreconditions.checkState( + mInputStateMap.size() == mInputMap.size(), + TAG, "mInputStateMap not the same size as mInputMap"); mContentRatingsManager.update(); } @@ -264,13 +336,12 @@ public class TvInputManagerHelper { mTvInputCustomLabels.clear(); mTvInputApplicationLabels.clear(); mTvInputApplicationIcons.clear(); - mTvInputAppliactionBanners.clear();; + mTvInputAppliactionBanners.clear(); + ; mInputIdToPartnerInputMap.clear(); } - /** - * Clears the TvInput labels map. - */ + /** Clears the TvInput labels map. */ public void clearTvInputLabels() { mTvInputLabels.clear(); mTvInputCustomLabels.clear(); @@ -294,8 +365,8 @@ public class TvInputManagerHelper { } /** - * Returns the default comparator for {@link TvInputInfo}. - * See {@link InputComparatorInternal} for detail. + * Returns the default comparator for {@link TvInputInfo}. See {@link InputComparatorInternal} + * for detail. */ public Comparator<TvInputInfo> getDefaultTvInputInfoComparator() { return mTvInputInfoComparator; @@ -304,35 +375,31 @@ public class TvInputManagerHelper { /** * Checks if the input is from a partner. * - * It's visible for comparator test. - * Package private is enough for this method, but public is necessary to workaround mockito - * bug. + * <p>It's visible for comparator test. Package private is enough for this method, but public is + * necessary to workaround mockito bug. */ @VisibleForTesting public boolean isPartnerInput(TvInputInfo inputInfo) { return isSystemInput(inputInfo) && !isBundledInput(inputInfo); } - /** - * Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set. - */ + /** Does the input have {@link ApplicationInfo#FLAG_SYSTEM} set. */ public boolean isSystemInput(TvInputInfo inputInfo) { return inputInfo != null - && (inputInfo.getServiceInfo().applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 0; + && (inputInfo.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) + != 0; } - /** - * Is the input one known bundled inputs not written by OEM/SOCs. - */ + /** Is the input one known bundled inputs not written by OEM/SOCs. */ public boolean isBundledInput(TvInputInfo inputInfo) { - return inputInfo != null && Utils.isInBundledPackageSet(inputInfo.getServiceInfo() - .applicationInfo.packageName); + return inputInfo != null + && CommonUtils.isInBundledPackageSet( + inputInfo.getServiceInfo().applicationInfo.packageName); } /** - * Returns if the given input is bundled and written by OEM/SOCs. - * This returns the cached result. + * Returns if the given input is bundled and written by OEM/SOCs. This returns the cached + * result. */ public boolean isPartnerInput(String inputId) { Boolean isPartnerInput = mInputIdToPartnerInputMap.get(inputId); @@ -348,9 +415,7 @@ public class TvInputManagerHelper { return mTvInputManager != null; } - /** - * Loads label of {@code info}. - */ + /** Loads label of {@code info}. */ public String loadLabel(TvInputInfo info) { String label = mTvInputLabels.get(info.getId()); if (label == null) { @@ -360,9 +425,7 @@ public class TvInputManagerHelper { return label; } - /** - * Loads custom label of {@code info} - */ + /** Loads custom label of {@code info} */ public String loadCustomLabel(TvInputInfo info) { String customLabel = mTvInputCustomLabels.get(info.getId()); if (customLabel == null) { @@ -375,60 +438,46 @@ public class TvInputManagerHelper { return customLabel; } - /** - * Gets the tv input application's label. - */ + /** Gets the tv input application's label. */ public CharSequence getTvInputApplicationLabel(CharSequence inputId) { return mTvInputApplicationLabels.get(inputId); } - /** - * Stores the tv input application's label. - */ + /** Stores the tv input application's label. */ public void setTvInputApplicationLabel(String inputId, CharSequence label) { mTvInputApplicationLabels.put(inputId, label); } - /** - * Gets the tv input application's icon. - */ + /** Gets the tv input application's icon. */ public Drawable getTvInputApplicationIcon(String inputId) { return mTvInputApplicationIcons.get(inputId); } - /** - * Stores the tv input application's icon. - */ + /** Stores the tv input application's icon. */ public void setTvInputApplicationIcon(String inputId, Drawable icon) { mTvInputApplicationIcons.put(inputId, icon); } - /** - * Gets the tv input application's banner. - */ + /** Gets the tv input application's banner. */ public Drawable getTvInputApplicationBanner(String inputId) { return mTvInputAppliactionBanners.get(inputId); } - /** - * Stores the tv input application's banner. - */ + /** Stores the tv input application's banner. */ public void setTvInputApplicationBanner(String inputId, Drawable banner) { mTvInputAppliactionBanners.put(inputId, banner); } - /** - * Returns if TV input exists with the input id. - */ + /** Returns if TV input exists with the input id. */ public boolean hasTvInputInfo(String inputId) { - SoftPreconditions.checkState(mStarted, TAG, - "hasTvInputInfo() called before TvInputManagerHelper was started."); + SoftPreconditions.checkState( + mStarted, TAG, "hasTvInputInfo() called before TvInputManagerHelper was started."); return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null; } public TvInputInfo getTvInputInfo(String inputId) { - SoftPreconditions.checkState(mStarted, TAG, - "getTvInputInfo() called before TvInputManagerHelper was started."); + SoftPreconditions.checkState( + mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started."); if (!mStarted) { return null; } @@ -452,16 +501,23 @@ public class TvInputManagerHelper { } return size; } - - public int getInputState(TvInputInfo inputInfo) { - return getInputState(inputInfo.getId()); + /** + * Returns TvInputInfo's input state. + * + * @param inputInfo + * @return An Integer which stands for the input state {@link + * TvInputManager.INPUT_STATE_DISCONNECTED} if inputInfo is null + */ + public int getInputState(@Nullable TvInputInfo inputInfo) { + return inputInfo == null + ? TvInputManager.INPUT_STATE_DISCONNECTED + : getInputState(inputInfo.getId()); } public int getInputState(String inputId) { SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started"); if (!mStarted) { return TvInputManager.INPUT_STATE_DISCONNECTED; - } Integer state = mInputStateMap.get(inputId); if (state == null) { @@ -483,16 +539,13 @@ public class TvInputManagerHelper { return mParentalControlSettings; } - /** - * Returns a ContentRatingsManager instance for a given application context. - */ + /** Returns a ContentRatingsManager instance for a given application context. */ public ContentRatingsManager getContentRatingsManager() { return mContentRatingsManager; } private int getInputSortKey(TvInputInfo input) { - return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, - Integer.MAX_VALUE); + return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, Integer.MAX_VALUE); } private boolean isInputPhysicalTuner(TvInputInfo input) { @@ -504,15 +557,20 @@ public class TvInputManagerHelper { if (input.createSetupIntent() == null) { return false; } else { - boolean mayBeTunerInput = mPackageManager.checkPermission( - PERMISSION_ACCESS_ALL_EPG_DATA, input.getServiceInfo().packageName) - == PackageManager.PERMISSION_GRANTED; + boolean mayBeTunerInput = + mPackageManager.checkPermission( + PERMISSION_ACCESS_ALL_EPG_DATA, + input.getServiceInfo().packageName) + == PackageManager.PERMISSION_GRANTED; if (!mayBeTunerInput) { try { - ApplicationInfo ai = mPackageManager.getApplicationInfo( - input.getServiceInfo().packageName, 0); - if ((ai.flags & (ApplicationInfo.FLAG_SYSTEM - | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) { + ApplicationInfo ai = + mPackageManager.getApplicationInfo( + input.getServiceInfo().packageName, 0); + if ((ai.flags + & (ApplicationInfo.FLAG_SYSTEM + | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) + == 0) { return false; } } catch (PackageManager.NameNotFoundException e) { @@ -524,14 +582,15 @@ public class TvInputManagerHelper { } private boolean isInBlackList(String inputId) { - if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + if (TvFeatures.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { if (inputId.contains(disabledTunerInputPrefix)) { return true; } } } - if (TvCommonUtils.isRunningInTest()) { + if (CommonUtils.isRoboTest()) return false; + if (CommonUtils.isRunningInTest()) { for (String testableInput : TESTABLE_INPUTS) { if (testableInput.equals(inputId)) { return false; @@ -545,10 +604,9 @@ public class TvInputManagerHelper { /** * Default comparator for TvInputInfo. * - * It's static class that accepts {@link TvInputManagerHelper} as parameter to test. - * To test comparator, we need to mock API in parent class such as {@link #isPartnerInput}, - * but it's impossible for an inner class to use mocked methods. - * (i.e. Mockito's spy doesn't work) + * <p>It's static class that accepts {@link TvInputManagerHelper} as parameter to test. To test + * comparator, we need to mock API in parent class such as {@link #isPartnerInput}, but it's + * impossible for an inner class to use mocked methods. (i.e. Mockito's spy doesn't work) */ @VisibleForTesting static class InputComparatorInternal implements Comparator<TvInputInfo> { @@ -568,8 +626,8 @@ public class TvInputManagerHelper { } /** - * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of - * TV inputs. + * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of TV + * inputs. */ public static class HardwareInputComparator implements Comparator<TvInputInfo> { private Map<Integer, Integer> mTypePriorities = new HashMap<>(); @@ -591,10 +649,12 @@ public class TvInputManagerHelper { return -1; } - boolean enabledL = (mTvInputManagerHelper.getInputState(lhs) - != TvInputManager.INPUT_STATE_DISCONNECTED); - boolean enabledR = (mTvInputManagerHelper.getInputState(rhs) - != TvInputManager.INPUT_STATE_DISCONNECTED); + boolean enabledL = + (mTvInputManagerHelper.getInputState(lhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); + boolean enabledR = + (mTvInputManagerHelper.getInputState(rhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); if (enabledL != enabledR) { return enabledL ? -1 : 1; } @@ -620,11 +680,13 @@ public class TvInputManagerHelper { return sortKeyR - sortKeyL; } - String parentLabelL = lhs.getParentId() != null - ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId())) + String parentLabelL = + lhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId())) : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId())); - String parentLabelR = rhs.getParentId() != null - ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId())) + String parentLabelR = + rhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId())) : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId())); if (!TextUtils.equals(parentLabelL, parentLabelR)) { diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index c5fde317..ae79e7e5 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -21,7 +21,6 @@ import android.content.SharedPreferences; import android.media.tv.TvTrackInfo; import android.preference.PreferenceManager; import android.support.annotation.IntDef; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -29,11 +28,11 @@ import java.util.HashSet; import java.util.Set; /** - * A class about the constants for TV settings. - * Objects that are returned from the various {@code get} methods must be treated as immutable. + * A class about the constants for TV settings. Objects that are returned from the various {@code + * get} methods must be treated as immutable. */ public final class TvSettings { - public static final String PREF_DISPLAY_MODE = "display_mode"; // int value + public static final String PREF_DISPLAY_MODE = "display_mode"; // int value public static final String PREF_PIN = "pin"; // 4-digit string value. Otherwise, it's not set. // Multi-track audio settings @@ -56,9 +55,14 @@ public final class TvSettings { @Retention(RetentionPolicy.SOURCE) @IntDef({ - CONTENT_RATING_LEVEL_NONE, CONTENT_RATING_LEVEL_HIGH, CONTENT_RATING_LEVEL_MEDIUM, - CONTENT_RATING_LEVEL_LOW, CONTENT_RATING_LEVEL_CUSTOM }) + CONTENT_RATING_LEVEL_NONE, + CONTENT_RATING_LEVEL_HIGH, + CONTENT_RATING_LEVEL_MEDIUM, + CONTENT_RATING_LEVEL_LOW, + CONTENT_RATING_LEVEL_CUSTOM + }) public @interface ContentRatingLevel {} + public static final int CONTENT_RATING_LEVEL_NONE = 0; public static final int CONTENT_RATING_LEVEL_HIGH = 1; public static final int CONTENT_RATING_LEVEL_MEDIUM = 2; @@ -69,61 +73,74 @@ public final class TvSettings { // Multi-track audio settings public static String getMultiAudioId(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getString( - PREF_MULTI_AUDIO_ID, null); + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(PREF_MULTI_AUDIO_ID, null); } public static void setMultiAudioId(Context context, String language) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putString( - PREF_MULTI_AUDIO_ID, language).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(PREF_MULTI_AUDIO_ID, language) + .apply(); } public static String getMultiAudioLanguage(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getString( - PREF_MULTI_AUDIO_LANGUAGE, null); + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(PREF_MULTI_AUDIO_LANGUAGE, null); } public static void setMultiAudioLanguage(Context context, String language) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putString( - PREF_MULTI_AUDIO_LANGUAGE, language).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(PREF_MULTI_AUDIO_LANGUAGE, language) + .apply(); } public static int getMultiAudioChannelCount(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_MULTI_AUDIO_CHANNEL_COUNT, 0); + return PreferenceManager.getDefaultSharedPreferences(context) + .getInt(PREF_MULTI_AUDIO_CHANNEL_COUNT, 0); } public static void setMultiAudioChannelCount(Context context, int channelCount) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount) + .apply(); } - public static void setDvrPlaybackTrackSettings(Context context, int trackType, - TvTrackInfo info) { + public static void setDvrPlaybackTrackSettings( + Context context, int trackType, TvTrackInfo info) { if (trackType == TvTrackInfo.TYPE_AUDIO) { if (info == null) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .remove(PREF_DVR_MULTI_AUDIO_ID).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove(PREF_DVR_MULTI_AUDIO_ID) + .apply(); } else { - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .putString(PREF_DVR_MULTI_AUDIO_LANGUAGE, info.getLanguage()) .putInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, info.getAudioChannelCount()) - .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()).apply(); + .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()) + .apply(); } } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { if (info == null) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .remove(PREF_DVR_SUBTITLE_ID).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove(PREF_DVR_SUBTITLE_ID) + .apply(); } else { - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .putString(PREF_DVR_SUBTITLE_LANGUAGE, info.getLanguage()) - .putString(PREF_DVR_SUBTITLE_ID, info.getId()).apply(); + .putString(PREF_DVR_SUBTITLE_ID, info.getId()) + .apply(); } } } - public static TvTrackInfo getDvrPlaybackTrackSettings(Context context, - int trackType) { + public static TvTrackInfo getDvrPlaybackTrackSettings(Context context, int trackType) { String language; String trackId; int channelCount; @@ -136,7 +153,9 @@ public final class TvSettings { language = pref.getString(PREF_DVR_MULTI_AUDIO_LANGUAGE, null); channelCount = pref.getInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, 0); return new TvTrackInfo.Builder(trackType, trackId) - .setLanguage(language).setAudioChannelCount(channelCount).build(); + .setLanguage(language) + .setAudioChannelCount(channelCount) + .build(); } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { trackId = pref.getString(PREF_DVR_SUBTITLE_ID, null); if (trackId == null) { @@ -153,16 +172,20 @@ public final class TvSettings { public static void addContentRatingSystem(Context context, String id) { Set<String> contentRatingSystemSet = getContentRatingSystemSet(context); if (contentRatingSystemSet.add(id)) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet) + .apply(); } } public static void removeContentRatingSystem(Context context, String id) { Set<String> contentRatingSystemSet = getContentRatingSystemSet(context); if (contentRatingSystemSet.remove(id)) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet) + .apply(); } } @@ -176,25 +199,28 @@ public final class TvSettings { */ public static boolean isContentRatingSystemSet(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) - .getStringSet(PREF_CONTENT_RATING_SYSTEMS, null) != null; + .getStringSet(PREF_CONTENT_RATING_SYSTEMS, null) + != null; } private static Set<String> getContentRatingSystemSet(Context context) { - return new HashSet<>(PreferenceManager.getDefaultSharedPreferences(context) - .getStringSet(PREF_CONTENT_RATING_SYSTEMS, Collections.emptySet())); + return new HashSet<>( + PreferenceManager.getDefaultSharedPreferences(context) + .getStringSet(PREF_CONTENT_RATING_SYSTEMS, Collections.emptySet())); } @ContentRatingLevel @SuppressWarnings("ResourceType") public static int getContentRatingLevel(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_CONTENT_RATING_LEVEL, CONTENT_RATING_LEVEL_NONE); + return PreferenceManager.getDefaultSharedPreferences(context) + .getInt(PREF_CONTENT_RATING_LEVEL, CONTENT_RATING_LEVEL_NONE); } - public static void setContentRatingLevel(Context context, - @ContentRatingLevel int level) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_CONTENT_RATING_LEVEL, level).apply(); + public static void setContentRatingLevel(Context context, @ContentRatingLevel int level) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(PREF_CONTENT_RATING_LEVEL, level) + .apply(); } /** @@ -202,8 +228,8 @@ public final class TvSettings { * repeatedly). */ public static long getDisablePinUntil(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getLong( - PREF_DISABLE_PIN_UNTIL, 0); + return PreferenceManager.getDefaultSharedPreferences(context) + .getLong(PREF_DISABLE_PIN_UNTIL, 0); } /** @@ -211,7 +237,9 @@ public final class TvSettings { * repeatedly). */ public static void setDisablePinUntil(Context context, long timeMillis) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putLong( - PREF_DISABLE_PIN_UNTIL, timeMillis).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putLong(PREF_DISABLE_PIN_UNTIL, timeMillis) + .apply(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java index 667cc9bf..09874502 100644 --- a/src/com/android/tv/util/TvTrackInfoUtils.java +++ b/src/com/android/tv/util/TvTrackInfoUtils.java @@ -16,27 +16,24 @@ package com.android.tv.util; import android.media.tv.TvTrackInfo; - import java.util.Comparator; import java.util.List; -/** - * Static utilities for {@link TvTrackInfo}. - */ +/** Static utilities for {@link TvTrackInfo}. */ public class TvTrackInfoUtils { /** * Compares how closely two {@link android.media.tv.TvTrackInfo}s match {@code language}, {@code * channelCount} and {@code id} in that precedence. * - * @param id The track id to match. - * @param language The language to match. + * @param id The track id to match. + * @param language The language to match. * @param channelCount The channel count to match. * @return -1 if lhs is a worse match, 0 if lhs and rhs match equally and 1 if lhs is a better - * match. + * match. */ - public static Comparator<TvTrackInfo> createComparator(final String id, final String language, - final int channelCount) { + public static Comparator<TvTrackInfo> createComparator( + final String id, final String language, final int channelCount) { return new Comparator<TvTrackInfo>() { @Override @@ -52,15 +49,17 @@ public class TvTrackInfoUtils { } // Assumes {@code null} language matches to any language since it means user hasn't // selected any track before or selected a track without language information. - boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(), - language); - boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), - language); + boolean lhsLangMatch = + language == null || Utils.isEqualLanguage(lhs.getLanguage(), language); + boolean rhsLangMatch = + language == null || Utils.isEqualLanguage(rhs.getLanguage(), language); if (lhsLangMatch && rhsLangMatch) { - boolean lhsCountMatch = lhs.getType() != TvTrackInfo.TYPE_AUDIO - || lhs.getAudioChannelCount() == channelCount; - boolean rhsCountMatch = rhs.getType() != TvTrackInfo.TYPE_AUDIO - || rhs.getAudioChannelCount() == channelCount; + boolean lhsCountMatch = + lhs.getType() != TvTrackInfo.TYPE_AUDIO + || lhs.getAudioChannelCount() == channelCount; + boolean rhsCountMatch = + rhs.getType() != TvTrackInfo.TYPE_AUDIO + || rhs.getAudioChannelCount() == channelCount; if (lhsCountMatch && rhsCountMatch) { return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); } else { @@ -74,16 +73,16 @@ public class TvTrackInfoUtils { } /** - * Selects the best TvTrackInfo available or the first if none matches. + * Selects the best TvTrackInfo available or the first if none matches. * - * @param tracks The tracks to choose from - * @param id The track id to match. - * @param language The language to match. + * @param tracks The tracks to choose from + * @param id The track id to match. + * @param language The language to match. * @param channelCount The channel count to match. * @return the best matching track or the first one if none matches. */ - public static TvTrackInfo getBestTrackInfo(List<TvTrackInfo> tracks, String id, String language, - int channelCount) { + public static TvTrackInfo getBestTrackInfo( + List<TvTrackInfo> tracks, String id, String language, int channelCount) { if (tracks == null) { return null; } @@ -97,6 +96,5 @@ public class TvTrackInfoUtils { return best; } - private TvTrackInfoUtils() { - } -}
\ No newline at end of file + private TvTrackInfoUtils() {} +} diff --git a/src/com/android/tv/util/TvUriMatcher.java b/src/com/android/tv/util/TvUriMatcher.java index 3d91cdad..9e74117e 100644 --- a/src/com/android/tv/util/TvUriMatcher.java +++ b/src/com/android/tv/util/TvUriMatcher.java @@ -21,22 +21,25 @@ import android.content.UriMatcher; import android.media.tv.TvContract; import android.net.Uri; import android.support.annotation.IntDef; - import com.android.tv.search.LocalSearchProvider; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Utility class to aid in matching URIs in TvProvider. - */ +/** Utility class to aid in matching URIs in TvProvider. */ public class TvUriMatcher { private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); @Retention(RetentionPolicy.SOURCE) - @IntDef({MATCH_CHANNEL, MATCH_CHANNEL_ID, MATCH_PROGRAM, MATCH_PROGRAM_ID, - MATCH_RECORDED_PROGRAM, MATCH_RECORDED_PROGRAM_ID, MATCH_WATCHED_PROGRAM_ID, - MATCH_ON_DEVICE_SEARCH}) + @IntDef({ + MATCH_CHANNEL, + MATCH_CHANNEL_ID, + MATCH_PROGRAM, + MATCH_PROGRAM_ID, + MATCH_RECORDED_PROGRAM, + MATCH_RECORDED_PROGRAM_ID, + MATCH_WATCHED_PROGRAM_ID, + MATCH_ON_DEVICE_SEARCH + }) private @interface TvProviderUriMatchCode {} /** The code for the channels URI. */ public static final int MATCH_CHANNEL = 1; @@ -54,6 +57,7 @@ public class TvUriMatcher { public static final int MATCH_WATCHED_PROGRAM_ID = 7; /** The code for the on-device search URI. */ public static final int MATCH_ON_DEVICE_SEARCH = 8; + static { URI_MATCHER.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); URI_MATCHER.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); @@ -62,11 +66,13 @@ public class TvUriMatcher { URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM); URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID); URI_MATCHER.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); - URI_MATCHER.addURI(LocalSearchProvider.AUTHORITY, - SearchManager.SUGGEST_URI_PATH_QUERY + "/*", MATCH_ON_DEVICE_SEARCH); + URI_MATCHER.addURI( + LocalSearchProvider.AUTHORITY, + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", + MATCH_ON_DEVICE_SEARCH); } - private TvUriMatcher() { } + private TvUriMatcher() {} /** * Try to match against the path in a url. @@ -74,7 +80,8 @@ public class TvUriMatcher { * @see UriMatcher#match */ @SuppressWarnings("WrongConstant") - @TvProviderUriMatchCode public static int match(Uri uri) { + @TvProviderUriMatchCode + public static int match(Uri uri) { return URI_MATCHER.match(uri); } } diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index d11bab3c..a75bd446 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -38,22 +38,16 @@ import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.text.format.DateUtils; -import android.util.ArraySet; import android.util.Log; import android.view.View; - -import com.android.tv.ApplicationSingletons; import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.BuildConfig; +import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; +import com.android.tv.common.util.Clock; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; -import com.android.tv.experiments.Experiments; - -import java.io.File; +import com.android.tv.data.api.Channel; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -69,18 +63,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -/** - * A class that includes convenience methods for accessing TvProvider database. - */ +/** A class that includes convenience methods for accessing TvProvider database. */ public class Utils { private static final String TAG = "Utils"; private static final boolean DEBUG = false; - private static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", - Locale.US); - public static final String EXTRA_KEY_ACTION = "action"; - public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input"; + public static final String EXTRA_ACTION_SHOW_TV_INPUT = "show_tv_input"; public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher"; public static final String EXTRA_KEY_RECORDED_PROGRAM_ID = "recorded_program_id"; public static final String EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME = "recorded_program_seek_time"; @@ -97,8 +86,7 @@ public class Utils { private static final String PREF_KEY_LAST_WATCHED_CHANNEL_URI = "last_watched_channel_uri"; private static final String PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID = "last_watched_tuner_input_id"; - private static final String PREF_KEY_RECORDING_FAILED_REASONS = - "recording_failed_reasons"; + private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons"; private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET = "failed_scheduled_recording_info_set"; @@ -121,15 +109,6 @@ public class Utils { private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); - // Hardcoded list for known bundled inputs not written by OEM/SOCs. - // Bundled (system) inputs not in the list will get the high priority - // so they and their channels come first in the UI. - private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>(); - - static { - BUNDLED_PACKAGE_SET.add("com.android.tv"); - } - private enum AspectRatio { ASPECT_RATIO_4_3(4, 3), ASPECT_RATIO_16_9(16, 9), @@ -150,13 +129,11 @@ public class Utils { } } - private Utils() { - } + private Utils() {} public static String buildSelectionForIds(String idName, List<Long> ids) { StringBuilder sb = new StringBuilder(); - sb.append(idName).append(" in (") - .append(ids.get(0)); + sb.append(idName).append(" in (").append(ids.get(0)); for (int i = 1; i < ids.size(); ++i) { sb.append(",").append(ids.get(i)); } @@ -171,8 +148,8 @@ public class Utils { } Uri channelUri = TvContract.buildChannelUri(channelId); String[] projection = {TvContract.Channels.COLUMN_INPUT_ID}; - try (Cursor cursor = context.getContentResolver() - .query(channelUri, projection, null, null, null)) { + try (Cursor cursor = + context.getContentResolver().query(channelUri, projection, null, null, null)) { if (cursor != null && cursor.moveToNext()) { return Utils.intern(cursor.getString(0)); } @@ -185,60 +162,61 @@ public class Utils { Log.e(TAG, "setLastWatchedChannel: channel cannot be null"); return; } - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, channel.getUri().toString()).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, channel.getUri().toString()) + .apply(); if (!channel.isPassthrough()) { long channelId = channel.getId(); if (channel.getId() < 0) { throw new IllegalArgumentException("channelId should be equal to or larger than 0"); } - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, channelId) - .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + channel.getInputId(), + .putLong( + PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + channel.getInputId(), channelId) .putString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, channel.getInputId()) .apply(); } } - /** - * Sets recording failed reason. - */ + /** Sets recording failed reason. */ public static void setRecordingFailedReason(Context context, int reason) { long reasons = getRecordingFailedReasons(context) | 0x1 << reason; - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons) .apply(); } - /** - * Adds the info of failed scheduled recording. - */ - public static void addFailedScheduledRecordingInfo(Context context, - String scheduledRecordingInfo) { + /** Adds the info of failed scheduled recording. */ + public static void addFailedScheduledRecordingInfo( + Context context, String scheduledRecordingInfo) { Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context); failedScheduledRecordingInfoSet.add(scheduledRecordingInfo); - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putStringSet( + PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, failedScheduledRecordingInfoSet) .apply(); } - /** - * Clears the failed scheduled recording info set. - */ + /** Clears the failed scheduled recording info set. */ public static void clearFailedScheduledRecordingInfoSet(Context context) { - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET) .apply(); } - /** - * Clears recording failed reason. - */ + /** Clears recording failed reason. */ public static void clearRecordingFailedReason(Context context, int reason) { long reasons = getRecordingFailedReasons(context) & ~(0x1 << reason); - PreferenceManager.getDefaultSharedPreferences(context).edit() + PreferenceManager.getDefaultSharedPreferences(context) + .edit() .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons) .apply(); } @@ -258,9 +236,7 @@ public class Utils { .getString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, null); } - /** - * Returns the last watched tuner input id. - */ + /** Returns the last watched tuner input id. */ public static String getLastWatchedTunerInputId(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null); @@ -268,32 +244,28 @@ public class Utils { private static long getRecordingFailedReasons(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) - .getLong(PREF_KEY_RECORDING_FAILED_REASONS, - RECORDING_FAILED_REASON_NONE); + .getLong(PREF_KEY_RECORDING_FAILED_REASONS, RECORDING_FAILED_REASON_NONE); } - /** - * Returns the failed scheduled recordings info set. - */ + /** Returns the failed scheduled recordings info set. */ public static Set<String> getFailedScheduledRecordingInfoSet(Context context) { return PreferenceManager.getDefaultSharedPreferences(context) .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); } - /** - * Checks do recording failed reason exist. - */ + /** Checks do recording failed reason exist. */ public static boolean hasRecordingFailedReason(Context context, int reason) { long reasons = getRecordingFailedReasons(context); return (reasons & 0x1 << reason) != 0; } /** - * Returns {@code true}, if {@code uri} specifies an input, which is usually generated - * from {@link TvContract#buildChannelsUriForInput}. + * Returns {@code true}, if {@code uri} specifies an input, which is usually generated from + * {@link TvContract#buildChannelsUriForInput}. */ public static boolean isChannelUriForInput(Uri uri) { - return isTvUri(uri) && PATH_CHANNEL.equals(uri.getPathSegments().get(0)) + return isTvUri(uri) + && PATH_CHANNEL.equals(uri.getPathSegments().get(0)) && !TextUtils.isEmpty(uri.getQueryParameter("input")); } @@ -314,7 +286,8 @@ public class Utils { } private static boolean isTvUri(Uri uri) { - return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + return uri != null + && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) && TvContract.AUTHORITY.equals(uri.getAuthority()); } @@ -323,23 +296,17 @@ public class Utils { return pathSegments.size() == 2 && pathSegment.equals(pathSegments.get(0)); } - /** - * Returns {@code true}, if {@code uri} is a programs URI. - */ + /** Returns {@code true}, if {@code uri} is a programs URI. */ public static boolean isProgramsUri(Uri uri) { return isTvUri(uri) && PATH_PROGRAM.equals(uri.getPathSegments().get(0)); } - /** - * Returns {@code true}, if {@code uri} is a programs URI. - */ + /** Returns {@code true}, if {@code uri} is a programs URI. */ public static boolean isRecordedProgramsUri(Uri uri) { return isTvUri(uri) && PATH_RECORDED_PROGRAM.equals(uri.getPathSegments().get(0)); } - /** - * Gets the info of the program on particular time. - */ + /** Gets the info of the program on particular time. */ @WorkerThread public static Program getProgramAt(Context context, long channelId, long timeMs) { if (channelId == Channel.INVALID_ID) { @@ -355,10 +322,11 @@ public class Utils { Log.w(TAG, message); } } - Uri uri = TvContract.buildProgramsUriForChannel(TvContract.buildChannelUri(channelId), - timeMs, timeMs); - try (Cursor cursor = context.getContentResolver().query(uri, Program.PROJECTION, - null, null, null)) { + Uri uri = + TvContract.buildProgramsUriForChannel( + TvContract.buildChannelUri(channelId), timeMs, timeMs); + try (Cursor cursor = + context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) { if (cursor != null && cursor.moveToNext()) { return Program.fromCursor(cursor); } @@ -366,55 +334,98 @@ public class Utils { return null; } - /** - * Gets the info of the current program. - */ + /** Gets the info of the current program. */ @WorkerThread public static Program getCurrentProgram(Context context, long channelId) { return getProgramAt(context, channelId, System.currentTimeMillis()); } - /** - * Returns the round off minutes when convert milliseconds to minutes. - */ + /** Returns the round off minutes when convert milliseconds to minutes. */ public static int getRoundOffMinsFromMs(long millis) { // Round off the result by adding half minute to the original ms. return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS); } /** - * Returns duration string according to the date & time format. - * If {@code startUtcMillis} and {@code endUtcMills} are equal, - * formatted time will be returned instead. + * Returns duration string according to the date & time format. If {@code startUtcMillis} and + * {@code endUtcMills} are equal, formatted time will be returned instead. * * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}. * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}. - * @param useShortFormat {@code true} if abbreviation is needed to save space. - * In that case, date will be omitted if duration starts from today - * and is less than a day. If it's necessary, - * {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. + * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case, + * date will be omitted if duration starts from today and is less than a day. If it's + * necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. */ public static String getDurationString( Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) { - return getDurationString(context, System.currentTimeMillis(), startUtcMillis, endUtcMillis, - useShortFormat, 0); + return getDurationString( + context, + System.currentTimeMillis(), + startUtcMillis, + endUtcMillis, + useShortFormat, + 0); } - @VisibleForTesting - static String getDurationString(Context context, long baseMillis, long startUtcMillis, - long endUtcMillis, boolean useShortFormat, int flag) { - return getDurationString(context, startUtcMillis, endUtcMillis, - useShortFormat, !isInGivenDay(baseMillis, startUtcMillis), true, flag); + /** + * Returns duration string according to the date & time format. If {@code startUtcMillis} and + * {@code endUtcMills} are equal, formatted time will be returned instead. + * + * @param clock the clock used to get the current time. + * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}. + * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}. + * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case, + * date will be omitted if duration starts from today and is less than a day. If it's + * necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. + */ + public static String getDurationString( + Context context, + Clock clock, + long startUtcMillis, + long endUtcMillis, + boolean useShortFormat) { + return getDurationString( + context, + clock.currentTimeMillis(), + startUtcMillis, + endUtcMillis, + useShortFormat, + 0); } - /** - * Returns duration string according to the time format, may not contain date information. - * Note: At least one of showDate and showTime should be true. + @VisibleForTesting + static String getDurationString( + Context context, + long baseMillis, + long startUtcMillis, + long endUtcMillis, + boolean useShortFormat, + int flag) { + return getDurationString( + context, + startUtcMillis, + endUtcMillis, + useShortFormat, + !isInGivenDay(baseMillis, startUtcMillis), + true, + flag); + } + + /** + * Returns duration string according to the time format, may not contain date information. Note: + * At least one of showDate and showTime should be true. */ - public static String getDurationString(Context context, long startUtcMillis, long endUtcMillis, - boolean useShortFormat, boolean showDate, boolean showTime, int flag) { - flag |= DateUtils.FORMAT_ABBREV_MONTH - | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); + public static String getDurationString( + Context context, + long startUtcMillis, + long endUtcMillis, + boolean useShortFormat, + boolean showDate, + boolean showTime, + int flag) { + flag |= + DateUtils.FORMAT_ABBREV_MONTH + | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); SoftPreconditions.checkArgument(showTime || showDate); if (showTime) { flag |= DateUtils.FORMAT_SHOW_TIME; @@ -431,20 +442,21 @@ public class Utils { // Do not show date for short format. // Subtracting one day is needed because {@link DateUtils@formatDateRange} // automatically shows date if the duration covers multiple days. - return DateUtils.formatDateRange(context, - startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag); + return DateUtils.formatDateRange( + context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag); } } // Workaround of b/28740989. // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM. String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag); - return startUtcMillis == endUtcMillis || dateRange.contains("–") ? dateRange + return startUtcMillis == endUtcMillis || dateRange.contains("–") + ? dateRange : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); } /** - * Checks if two given time (in milliseconds) are in the same day with regard to the - * locale timezone. + * Checks if two given time (in milliseconds) are in the same day with regard to the locale + * timezone. */ public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { TimeZone timeZone = Calendar.getInstance().getTimeZone(); @@ -456,9 +468,7 @@ public class Utils { == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); } - /** - * Calculate how many days between two milliseconds. - */ + /** Calculate how many days between two milliseconds. */ public static int computeDateDifference(long startTimeMs, long endTimeMs) { Calendar calFrom = Calendar.getInstance(); Calendar calTo = Calendar.getInstance(); @@ -476,17 +486,23 @@ public class Utils { cal.set(Calendar.MILLISECOND, 0); } - /** - * Returns the last millisecond of a day which the millis belongs to. - */ + /** Returns the last millisecond of a day which the millis belongs to. */ public static long getLastMillisecondOfDay(long millis) { - Calendar calender = Calendar.getInstance(); - calender.setTime(new Date(millis)); - calender.set(Calendar.HOUR_OF_DAY, 23); - calender.set(Calendar.MINUTE, 59); - calender.set(Calendar.SECOND, 59); - calender.set(Calendar.MILLISECOND, 999); - return calender.getTimeInMillis(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date(millis)); + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + return calendar.getTimeInMillis(); + } + + /** Returns the last millisecond of a day which the millis belongs to. */ + public static long getFirstMillisecondOfDay(long millis) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date(millis)); + resetCalendar(calendar); + return calendar.getTimeInMillis(); } public static String getAspectRatioString(int width, int height) { @@ -494,7 +510,7 @@ public class Utils { return ""; } - for (AspectRatio ratio: AspectRatio.values()) { + for (AspectRatio ratio : AspectRatio.values()) { if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) { return ratio.toString(); } @@ -531,11 +547,9 @@ public class Utils { public static String getVideoDefinitionLevelString(Context context, int videoFormat) { switch (videoFormat) { case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD: - return context.getResources().getString( - R.string.video_definition_level_ultra_hd); + return context.getResources().getString(R.string.video_definition_level_ultra_hd); case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD: - return context.getResources().getString( - R.string.video_definition_level_full_hd); + return context.getResources().getString(R.string.video_definition_level_full_hd); case StreamInfo.VIDEO_DEFINITION_LEVEL_HD: return context.getResources().getString(R.string.video_definition_level_hd); case StreamInfo.VIDEO_DEFINITION_LEVEL_SD: @@ -570,8 +584,8 @@ public class Utils { return false; } - public static String getMultiAudioString(Context context, TvTrackInfo track, - boolean showSampleRate) { + public static String getMultiAudioString( + Context context, TvTrackInfo track, boolean showSampleRate) { if (track.getType() != TvTrackInfo.TYPE_AUDIO) { throw new IllegalArgumentException("Not an audio track: " + track); } @@ -600,11 +614,17 @@ public class Utils { break; default: if (track.getAudioChannelCount() > 0) { - metadata.append(context.getString(R.string.multi_audio_channel_suffix, - track.getAudioChannelCount())); + metadata.append( + context.getString( + R.string.multi_audio_channel_suffix, + track.getAudioChannelCount())); } else { - Log.d(TAG, "Invalid audio channel count (" + track.getAudioChannelCount() - + ") found for the audio track: " + track); + Log.d( + TAG, + "Invalid audio channel count (" + + track.getAudioChannelCount() + + ") found for the audio track: " + + track); } break; } @@ -628,8 +648,8 @@ public class Utils { if (metadata.length() == 0) { return language; } - return context.getString(R.string.multi_audio_display_string_with_channel, language, - metadata.toString()); + return context.getString( + R.string.multi_audio_display_string_with_channel, language, metadata.toString()); } public static boolean isEqualLanguage(String lang1, String lang2) { @@ -647,19 +667,19 @@ public class Utils { } public static boolean isIntentAvailable(Context context, Intent intent) { - return context.getPackageManager().queryIntentActivities( - intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; + return context.getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + .size() + > 0; } - /** - * Returns the label for a given input. Returns the custom label, if any. - */ + /** Returns the label for a given input. Returns the custom label, if any. */ public static String loadLabel(Context context, TvInputInfo input) { if (input == null) { return null; } TvInputManagerHelper inputManager = - TvApplication.getSingletons(context).getTvInputManagerHelper(); + TvSingletons.getSingletons(context).getTvInputManagerHelper(); CharSequence customLabel = inputManager.loadCustomLabel(input); String label = (customLabel == null) ? null : customLabel.toString(); if (TextUtils.isEmpty(label)) { @@ -668,9 +688,7 @@ public class Utils { return label; } - /** - * Enable all channels synchronously. - */ + /** Enable all channels synchronously. */ @WorkerThread public static void enableAllChannels(Context context) { ContentValues values = new ContentValues(); @@ -681,46 +699,48 @@ public class Utils { /** * Converts time in milliseconds to a String. * - * @param fullFormat {@code true} for returning date string with a full format - * (e.g., Mon Aug 15 20:08:35 GMT 2016). {@code false} for a short format, - * {e.g., [8/15/16] 8:08 AM}, in which date information would only appears - * when the target time is not today. + * @param fullFormat {@code true} for returning date string with a full format (e.g., Mon Aug 15 + * 20:08:35 GMT 2016). {@code false} for a short format, {e.g., 8/15/16 or 8:08 AM}, in + * which only the time is shown if the time is on the same day as now, and only the date is + * shown if it's a different day. */ public static String toTimeString(long timeMillis, boolean fullFormat) { if (fullFormat) { return new Date(timeMillis).toString(); } else { long currentTime = System.currentTimeMillis(); - return (String) DateUtils.formatSameDayTime(timeMillis, System.currentTimeMillis(), - SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); + return (String) + DateUtils.formatSameDayTime( + timeMillis, + System.currentTimeMillis(), + SimpleDateFormat.SHORT, + SimpleDateFormat.SHORT); } } - /** - * Converts time in milliseconds to a String. - */ + /** Converts time in milliseconds to a String. */ public static String toTimeString(long timeMillis) { return toTimeString(timeMillis, true); } /** - * Converts time in milliseconds to a ISO 8061 string. - */ - public static String toIsoDateTimeString(long timeMillis) { - return ISO_8601.format(new Date(timeMillis)); - } - - /** * Returns a {@link String} object which contains the layout information of the {@code view}. */ public static String toRectString(View view) { return "{" - + "l=" + view.getLeft() - + ",r=" + view.getRight() - + ",t=" + view.getTop() - + ",b=" + view.getBottom() - + ",w=" + view.getWidth() - + ",h=" + view.getHeight() + "}"; + + "l=" + + view.getLeft() + + ",r=" + + view.getRight() + + ",t=" + + view.getTop() + + ",b=" + + view.getBottom() + + ",w=" + + view.getWidth() + + ",h=" + + view.getHeight() + + "}"; } /** @@ -732,16 +752,14 @@ public class Utils { } /** - * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is - * one hour (60 * 60 * 1000), then the output will be 6:00:00. + * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is one + * hour (60 * 60 * 1000), then the output will be 6:00:00. */ public static long ceilTime(long timeMs, long timeUnit) { return timeMs + timeUnit - (timeMs % timeUnit); } - /** - * Returns an {@link String#intern() interned} string or null if the input is null. - */ + /** Returns an {@link String#intern() interned} string or null if the input is null. */ @Nullable public static String intern(@Nullable String string) { return string == null ? null : string.intern(); @@ -749,6 +767,7 @@ public class Utils { /** * Check if the index is valid for the collection, + * * @param collection the collection * @param index the index position to test * @return index >= 0 && index < collection.size(). @@ -757,9 +776,7 @@ public class Utils { return collection != null && (index >= 0 && index < collection.size()); } - /** - * Returns a localized version of the text resource specified by resourceId. - */ + /** Returns a localized version of the text resource specified by resourceId. */ public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) { if (locale.equals(context.getResources().getConfiguration().locale)) { return context.getText(resourceId); @@ -769,12 +786,12 @@ public class Utils { return context.createConfigurationContext(config).getText(resourceId); } - /** - * Checks where there is any internal TV input. - */ + /** Checks where there is any internal TV input. */ public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) { - for (TvInputInfo input : TvApplication.getSingletons(context).getTvInputManagerHelper() - .getTvInputInfos(true, tunerInputOnly)) { + for (TvInputInfo input : + TvSingletons.getSingletons(context) + .getTvInputManagerHelper() + .getTvInputInfos(true, tunerInputOnly)) { if (isInternalTvInput(context, input.getId())) { return true; } @@ -782,13 +799,13 @@ public class Utils { return false; } - /** - * Returns the internal TV inputs. - */ + /** Returns the internal TV inputs. */ public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) { List<TvInputInfo> inputs = new ArrayList<>(); - for (TvInputInfo input : TvApplication.getSingletons(context).getTvInputManagerHelper() - .getTvInputInfos(true, tunerInputOnly)) { + for (TvInputInfo input : + TvSingletons.getSingletons(context) + .getTvInputManagerHelper() + .getTvInputInfos(true, tunerInputOnly)) { if (isInternalTvInput(context, input.getId())) { inputs.add(input); } @@ -796,81 +813,41 @@ public class Utils { return inputs; } - /** - * Checks whether the input is internal or not. - */ + /** Checks whether the input is internal or not. */ public static boolean isInternalTvInput(Context context, String inputId) { - return context.getPackageName().equals(ComponentName.unflattenFromString(inputId) - .getPackageName()); + return context.getPackageName() + .equals(ComponentName.unflattenFromString(inputId).getPackageName()); } - /** - * Returns the TV input for the given {@code program}. - */ + /** Returns the TV input for the given {@code program}. */ @Nullable public static TvInputInfo getTvInputInfoForProgram(Context context, Program program) { - if (!Program.isValid(program)) { + if (!Program.isProgramValid(program)) { return null; } return getTvInputInfoForChannelId(context, program.getChannelId()); } - /** - * Returns the TV input for the given channel ID. - */ + /** Returns the TV input for the given channel ID. */ @Nullable public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) { - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - Channel channel = appSingletons.getChannelDataManager().getChannel(channelId); + TvSingletons tvSingletons = TvSingletons.getSingletons(context); + Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId); if (channel == null) { return null; } - return appSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); + return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); } - /** - * Returns the {@link TvInputInfo} for the given input ID. - */ + /** Returns the {@link TvInputInfo} for the given input ID. */ @Nullable public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) { - return TvApplication.getSingletons(context).getTvInputManagerHelper() + return TvSingletons.getSingletons(context) + .getTvInputManagerHelper() .getTvInputInfo(inputId); } - /** - * Deletes a file or a directory. - */ - public static void deleteDirOrFile(File fileOrDirectory) { - if (fileOrDirectory.isDirectory()) { - for (File child : fileOrDirectory.listFiles()) { - deleteDirOrFile(child); - } - } - fileOrDirectory.delete(); - } - - /** - * Checks whether a given package is in our bundled package set. - */ - public static boolean isInBundledPackageSet(String packageName) { - return BUNDLED_PACKAGE_SET.contains(packageName); - } - - /** - * Checks whether a given input is a bundled input. - */ - public static boolean isBundledInput(String inputId) { - for (String prefix : BUNDLED_PACKAGE_SET) { - if (inputId.startsWith(prefix + "/")) { - return true; - } - } - return false; - } - - /** - * Returns the canonical genre ID's from the {@code genres}. - */ + /** Returns the canonical genre ID's from the {@code genres}. */ public static int[] getCanonicalGenreIds(String genres) { if (TextUtils.isEmpty(genres)) { return null; @@ -878,9 +855,7 @@ public class Utils { return getCanonicalGenreIds(Genres.decode(genres)); } - /** - * Returns the canonical genre ID's from the {@code genres}. - */ + /** Returns the canonical genre ID's from the {@code genres}. */ public static int[] getCanonicalGenreIds(String[] canonicalGenres) { if (canonicalGenres != null && canonicalGenres.length > 0) { int[] results = new int[canonicalGenres.length]; @@ -901,9 +876,7 @@ public class Utils { return null; } - /** - * Returns the canonical genres for database. - */ + /** Returns the canonical genres for database. */ public static String getCanonicalGenre(int[] canonicalGenreIds) { if (canonicalGenreIds == null || canonicalGenreIds.length == 0) { return null; @@ -916,15 +889,8 @@ public class Utils { } /** - * Returns true if the current user is a developer. - */ - public static boolean isDeveloper() { - return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get(); - } - - /** - * Runs the method in main thread. If the current thread is not main thread, block it util - * the method is finished. + * Runs the method in main thread. If the current thread is not main thread, block it util the + * method is finished. */ public static void runInMainThreadAndWait(Runnable runnable) { if (Looper.myLooper() == Looper.getMainLooper()) { diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java index ed9a8ff6..b8bdb6b8 100644 --- a/src/com/android/tv/util/ViewCache.java +++ b/src/com/android/tv/util/ViewCache.java @@ -1,3 +1,18 @@ +/* + * 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.util; import android.content.Context; @@ -5,22 +20,17 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import java.util.ArrayList; -/** - * A cache for the views. - */ +/** A cache for the views. */ public class ViewCache { - private final static SparseArray<ArrayList<View>> mViews = new SparseArray(); + private static final SparseArray<ArrayList<View>> mViews = new SparseArray(); private static ViewCache sViewCache; - private ViewCache() { } + private ViewCache() {} - /** - * Returns an instance of the view cache. - */ + /** Returns an instance of the view cache. */ public static ViewCache getInstance() { if (sViewCache == null) { sViewCache = new ViewCache(); @@ -28,16 +38,12 @@ public class ViewCache { return sViewCache; } - /** - * Returns if the view cache is empty. - */ + /** Returns if the view cache is empty. */ public boolean isEmpty() { return mViews.size() == 0; } - /** - * Stores a view into this view cache. - */ + /** Stores a view into this view cache. */ public void putView(int resId, View view) { ArrayList<View> views = mViews.get(resId); if (views == null) { @@ -47,9 +53,7 @@ public class ViewCache { views.add(view); } - /** - * Stores multi specific views into the view cache. - */ + /** Stores multi specific views into the view cache. */ public void putView(Context context, int resId, ViewGroup fakeParent, int num) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -64,9 +68,7 @@ public class ViewCache { } } - /** - * Returns the view for specific resource id. - */ + /** Returns the view for specific resource id. */ public View getView(int resId) { ArrayList<View> views = mViews.get(resId); if (views != null && !views.isEmpty()) { @@ -80,9 +82,7 @@ public class ViewCache { } } - /** - * Returns the view if exists, or create a new view for the specific resource id. - */ + /** Returns the view if exists, or create a new view for the specific resource id. */ public View getOrCreateView(LayoutInflater inflater, int resId, ViewGroup container) { View view = getView(resId); if (view == null) { @@ -91,9 +91,7 @@ public class ViewCache { return view; } - /** - * Clears the view cache. - */ + /** Clears the view cache. */ public void clear() { mViews.clear(); } diff --git a/src/com/android/tv/util/account/AccountHelper.java b/src/com/android/tv/util/account/AccountHelper.java new file mode 100644 index 00000000..e98b42ec --- /dev/null +++ b/src/com/android/tv/util/account/AccountHelper.java @@ -0,0 +1,38 @@ +/* + * 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.util.account; + +import android.accounts.Account; +import android.support.annotation.Nullable; + +/** Helper methods for getting and selecting a user account. */ +public interface AccountHelper { + /** Returns the currently selected account or {@code null} if none is selected. */ + @Nullable + Account getSelectedAccount(); + /** + * Selects the first account available. + * + * @return selected account or {@code null} if none is selected. + */ + @Nullable + Account selectFirstAccount(); + + /** Returns all eligible accounts . */ + @Nullable + Account getFirstEligibleAccount(); +} diff --git a/src/com/android/tv/util/AccountHelper.java b/src/com/android/tv/util/account/AccountHelperImpl.java index ece13de1..58fbd27e 100644 --- a/src/com/android/tv/util/AccountHelper.java +++ b/src/com/android/tv/util/account/AccountHelperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -14,43 +14,32 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.util.account; import android.accounts.Account; import android.content.Context; import android.content.SharedPreferences; -import android.os.RemoteException; import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.util.Log; - -import java.util.Arrays; - -/** - * Helper methods for getting and selecting a user account. - */ -public class AccountHelper { - private static final String TAG = "AccountHelper"; - private static final boolean DEBUG = false; +/** Helper methods for getting and selecting a user account. */ +public class AccountHelperImpl implements com.android.tv.util.account.AccountHelper { private static final String SELECTED_ACCOUNT = "android.tv.livechannels.selected_account"; - private final Context mContext; + protected final Context mContext; private final SharedPreferences mDefaultPreferences; - @Nullable - private Account mSelectedAccount; + @Nullable private Account mSelectedAccount; - public AccountHelper(Context context) { + public AccountHelperImpl(Context context) { mContext = context.getApplicationContext(); mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); } - /** - * Returns the currently selected account or {@code null} if none is selected. - */ + /** Returns the currently selected account or {@code null} if none is selected. */ + @Override @Nullable - public Account getSelectedAccount() { + public final Account getSelectedAccount() { String accountId = mDefaultPreferences.getString(SELECTED_ACCOUNT, null); if (accountId == null) { return null; @@ -68,9 +57,11 @@ public class AccountHelper { } /** - * Returns all eligible accounts . + * Returns all eligible accounts. + * + * <p>Override this method to return the accounts needed. */ - private Account[] getEligibleAccounts() { + protected Account[] getEligibleAccounts() { return new Account[0]; } @@ -79,8 +70,9 @@ public class AccountHelper { * * @return selected account or {@code null} if none is selected. */ + @Override @Nullable - public Account selectFirstAccount() { + public final Account selectFirstAccount() { Account account = getFirstEligibleAccount(); if (account != null) { selectAccount(account); @@ -93,19 +85,17 @@ public class AccountHelper { * * @return first account or {@code null} if none is eligible. */ + @Override @Nullable - public Account getFirstEligibleAccount() { + public final Account getFirstEligibleAccount() { Account[] accounts = getEligibleAccounts(); return accounts.length == 0 ? null : accounts[0]; } - /** - * Sets the given account as the selected account. - */ + /** Sets the given account as the selected account. */ private void selectAccount(Account account) { - SharedPreferences defaultPreferences = PreferenceManager - .getDefaultSharedPreferences(mContext); + SharedPreferences defaultPreferences = + PreferenceManager.getDefaultSharedPreferences(mContext); defaultPreferences.edit().putString(SELECTED_ACCOUNT, account.name).commit(); } } - diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/images/BitmapUtils.java index fbaab023..d6bd5a31 100644 --- a/src/com/android/tv/util/BitmapUtils.java +++ b/src/com/android/tv/util/images/BitmapUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.util.images; import android.content.ContentResolver; import android.content.Context; @@ -29,7 +29,7 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; - +import com.android.tv.common.util.NetworkTrafficTags; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.IOException; @@ -45,12 +45,14 @@ public final class BitmapUtils { // The value of 64K, for MARK_READ_LIMIT, is chosen to be eight times the default buffer size // of BufferedInputStream (8K) allowing it to double its buffers three times. Also it is a // fairly reasonable value, not using too much memory and being large enough for most cases. - private static final int MARK_READ_LIMIT = 64 * 1024; // 64K + private static final int MARK_READ_LIMIT = 64 * 1024; // 64K - private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec - private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec + private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec + private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec - private BitmapUtils() { /* cannot be instantiated */ } + private BitmapUtils() { + /* cannot be instantiated */ + } public static Bitmap scaleBitmap(Bitmap bm, int maxWidth, int maxHeight) { Rect rect = calculateNewSize(bm, maxWidth, maxHeight); @@ -59,7 +61,8 @@ public final class BitmapUtils { public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) { Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight); - return scaledBitmap.isMutable() ? scaledBitmap + return scaledBitmap.isMutable() + ? scaledBitmap : scaledBitmap.copy(Bitmap.Config.ARGB_8888, true); } @@ -77,17 +80,17 @@ public final class BitmapUtils { return rect; } - public static ScaledBitmapInfo createScaledBitmapInfo(String id, Bitmap bm, int maxWidth, - int maxHeight) { - return new ScaledBitmapInfo(id, scaleBitmap(bm, maxWidth, maxHeight), + public static ScaledBitmapInfo createScaledBitmapInfo( + String id, Bitmap bm, int maxWidth, int maxHeight) { + return new ScaledBitmapInfo( + id, + scaleBitmap(bm, maxWidth, maxHeight), calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight)); } - /** - * Decode large sized bitmap into requested size. - */ - public static ScaledBitmapInfo decodeSampledBitmapFromUriString(Context context, - String uriString, int reqWidth, int reqHeight) { + /** Decode large sized bitmap into requested size. */ + public static ScaledBitmapInfo decodeSampledBitmapFromUriString( + Context context, String uriString, int reqWidth, int reqHeight) { if (TextUtils.isEmpty(uriString)) { return null; } @@ -162,8 +165,8 @@ public final class BitmapUtils { return urlConnection; } - private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, - int reqHeight) { + private static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); } @@ -187,7 +190,7 @@ public final class BitmapUtils { closeable.close(); } catch (IOException e) { // Log and continue. - Log.w(TAG,"Error closing " + closeable, e); + Log.w(TAG, "Error closing " + closeable, e); } } if (urlConnection instanceof HttpURLConnection) { @@ -195,21 +198,13 @@ public final class BitmapUtils { } } - /** - * A wrapper class which contains the loaded bitmap and the scaling information. - */ + /** A wrapper class which contains the loaded bitmap and the scaling information. */ public static class ScaledBitmapInfo { - /** - * The id of bitmap, usually this is the URI of the original. - */ - @NonNull - public final String id; + /** The id of bitmap, usually this is the URI of the original. */ + @NonNull public final String id; - /** - * The loaded bitmap object. - */ - @NonNull - public final Bitmap bitmap; + /** The loaded bitmap object. */ + @NonNull public final Bitmap bitmap; /** * The scaling factor to the original bitmap. It should be an positive integer. @@ -222,8 +217,8 @@ public final class BitmapUtils { * A constructor. * * @param bitmap The loaded bitmap object. - * @param inSampleSize The sampling size. - * See {@link android.graphics.BitmapFactory.Options#inSampleSize} + * @param inSampleSize The sampling size. See {@link + * android.graphics.BitmapFactory.Options#inSampleSize} */ public ScaledBitmapInfo(@NonNull String id, @NonNull Bitmap bitmap, int inSampleSize) { this.id = id; @@ -232,10 +227,9 @@ public final class BitmapUtils { } /** - * Checks if the bitmap needs to be reloaded. The scaling is performed by power 2. - * The bitmap can be reloaded only if the required width or height is greater then or equal - * to the existing bitmap. - * If the full sized bitmap is already loaded, returns {@code false}. + * Checks if the bitmap needs to be reloaded. The scaling is performed by power 2. The + * bitmap can be reloaded only if the required width or height is greater then or equal to + * the existing bitmap. If the full sized bitmap is already loaded, returns {@code false}. * * @see android.graphics.BitmapFactory.Options#inSampleSize */ @@ -245,26 +239,41 @@ public final class BitmapUtils { return false; } Rect size = calculateNewSize(this.bitmap, reqWidth, reqHeight); - boolean reload = (size.right >= bitmap.getWidth() * 2 - || size.bottom >= bitmap.getHeight() * 2); + boolean reload = + (size.right >= bitmap.getWidth() * 2 || size.bottom >= bitmap.getHeight() * 2); if (DEBUG) { - Log.d(TAG, "needToReload(" + reqWidth + ", " + reqHeight + ")=" + reload - + " because the new size would be " + size + " for " + this); + Log.d( + TAG, + "needToReload(" + + reqWidth + + ", " + + reqHeight + + ")=" + + reload + + " because the new size would be " + + size + + " for " + + this); } return reload; } - /** - * Returns {@code true} if a request the size of {@code other} would need a reload. - */ - public boolean needToReload(ScaledBitmapInfo other){ + /** Returns {@code true} if a request the size of {@code other} would need a reload. */ + public boolean needToReload(ScaledBitmapInfo other) { return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight()); } @Override public String toString() { - return "ScaledBitmapInfo[" + id + "](in=" + inSampleSize + ", w=" + bitmap.getWidth() - + ", h=" + bitmap.getHeight() + ")"; + return "ScaledBitmapInfo[" + + id + + "](in=" + + inSampleSize + + ", w=" + + bitmap.getWidth() + + ", h=" + + bitmap.getHeight() + + ")"; } } diff --git a/src/com/android/tv/util/ImageCache.java b/src/com/android/tv/util/images/ImageCache.java index b413c364..e260d67a 100644 --- a/src/com/android/tv/util/ImageCache.java +++ b/src/com/android/tv/util/images/ImageCache.java @@ -14,18 +14,15 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.util.images; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.LruCache; +import com.android.tv.common.memory.MemoryManageable; +import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; -import com.android.tv.common.MemoryManageable; -import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; - -/** - * A convenience class for caching bitmap. - */ +/** A convenience class for caching bitmap. */ public class ImageCache implements MemoryManageable { private static final float MAX_CACHE_SIZE_PERCENT = 0.8f; private static final float MIN_CACHE_SIZE_PERCENT = 0.05f; @@ -48,16 +45,17 @@ public class ImageCache implements MemoryManageable { if (DEBUG) { Log.d(TAG, "Memory cache created (size = " + memCacheSize + " Kbytes)"); } - mMemoryCache = new LruCache<String, ScaledBitmapInfo>(memCacheSize) { - /** - * Measure item size in kilobytes rather than units which is more practical for a bitmap - * cache - */ - @Override - protected int sizeOf(String key, ScaledBitmapInfo bitmapInfo) { - return (bitmapInfo.bitmap.getByteCount() + 1023) / 1024; - } - }; + mMemoryCache = + new LruCache<String, ScaledBitmapInfo>(memCacheSize) { + /** + * Measure item size in kilobytes rather than units which is more practical for + * a bitmap cache + */ + @Override + protected int sizeOf(String key, ScaledBitmapInfo bitmapInfo) { + return (bitmapInfo.bitmap.getByteCount() + 1023) / 1024; + } + }; } private static ImageCache sImageCache; @@ -67,7 +65,7 @@ public class ImageCache implements MemoryManageable { * param. * * @param memCacheSizePercent The cache size as a percent of available app memory. Should be in - * range of MIN_CACHE_SIZE_PERCENT(0.05) ~ MAX_CACHE_SIZE_PERCENT(0.8). + * range of MIN_CACHE_SIZE_PERCENT(0.05) ~ MAX_CACHE_SIZE_PERCENT(0.8). * @return An existing retained ImageCache object or a new one if one did not exist */ public static synchronized ImageCache getInstance(float memCacheSizePercent) { @@ -82,7 +80,6 @@ public class ImageCache implements MemoryManageable { return new ImageCache(memCacheSizePercent); } - /** * Returns an existing ImageCache, if it doesn't exist, a new one is created using * DEFAULT_CACHE_SIZE_PERCENT (0.1). @@ -96,8 +93,8 @@ public class ImageCache implements MemoryManageable { /** * Adds a bitmap to memory cache. * - * <p>If there is an existing bitmap only replace it if - * {@link ScaledBitmapInfo#needToReload(ScaledBitmapInfo)} is true. + * <p>If there is an existing bitmap only replace it if {@link + * ScaledBitmapInfo#needToReload(ScaledBitmapInfo)} is true. * * @param bitmapInfo The {@link ScaledBitmapInfo} object to store */ @@ -112,14 +109,25 @@ public class ImageCache implements MemoryManageable { if (old != null && !old.needToReload(bitmapInfo)) { mMemoryCache.put(key, old); if (DEBUG) { - Log.d(TAG, - "Kept original " + old + " in memory cache because it was larger than " - + bitmapInfo + "."); + Log.d( + TAG, + "Kept original " + + old + + " in memory cache because it was larger than " + + bitmapInfo + + "."); } } else { if (DEBUG) { - Log.d(TAG, "Add " + bitmapInfo + " to memory cache. Current size is " + - mMemoryCache.size() + " / " + mMemoryCache.maxSize() + " Kbytes"); + Log.d( + TAG, + "Add " + + bitmapInfo + + " to memory cache. Current size is " + + mMemoryCache.size() + + " / " + + mMemoryCache.maxSize() + + " Kbytes"); } } } @@ -158,19 +166,21 @@ public class ImageCache implements MemoryManageable { * Calculates the memory cache size based on a percentage of the max available VM memory. Eg. * setting percent to 0.2 would set the memory cache to one fifth of the available memory. * Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. memCacheSize is stored - * in kilobytes instead of bytes as this will eventually be passed to construct a LruCache - * which takes an int in its constructor. This value should be chosen carefully based on a - * number of factors Refer to the corresponding Android Training class for more discussion: + * in kilobytes instead of bytes as this will eventually be passed to construct a LruCache which + * takes an int in its constructor. This value should be chosen carefully based on a number of + * factors Refer to the corresponding Android Training class for more discussion: * http://developer.android.com/training/displaying-bitmaps/ * * @param percent Percent of available app memory to use to size memory cache. */ public static int calculateMemCacheSize(float percent) { if (percent < MIN_CACHE_SIZE_PERCENT || percent > MAX_CACHE_SIZE_PERCENT) { - throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " - + "between 0.05 and 0.8 (inclusive)"); + throw new IllegalArgumentException( + "setMemCacheSizePercent - percent must be " + + "between 0.05 and 0.8 (inclusive)"); } - return Math.max(MIN_CACHE_SIZE_KBYTES, + return Math.max( + MIN_CACHE_SIZE_KBYTES, Math.round(percent * Runtime.getRuntime().maxMemory() / 1024)); } diff --git a/src/com/android/tv/util/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java index 86bb94c1..e844e2ca 100644 --- a/src/com/android/tv/util/ImageLoader.java +++ b/src/com/android/tv/util/images/ImageLoader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.util; +package com.android.tv.util.images; import android.content.Context; import android.graphics.Bitmap; @@ -30,10 +30,9 @@ import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; import android.util.ArraySet; import android.util.Log; - import com.android.tv.R; -import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; - +import com.android.tv.common.concurrent.NamedThreadFactory; +import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; @@ -47,8 +46,8 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** - * This class wraps up completing some arbitrary long running work when loading a bitmap. It - * handles things like using a memory cache, running the work in a background thread. + * This class wraps up completing some arbitrary long running work when loading a bitmap. It handles + * things like using a memory cache, running the work in a background thread. */ public final class ImageLoader { private static final String TAG = "ImageLoader"; @@ -69,19 +68,23 @@ public final class ImageLoader { /** * An private {@link Executor} that can be used to execute tasks in parallel. * - * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask} - * Since we do a lot of concurrent image loading we can exhaust a thread pool. - * ImageLoader catches the error, and just leaves the image blank. - * However other tasks will fail and crash the application. + * <p>{@code IMAGE_THREAD_POOL_EXECUTOR} setting are copied from {@link AsyncTask} Since we do a + * lot of concurrent image loading we can exhaust a thread pool. ImageLoader catches the error, + * and just leaves the image blank. However other tasks will fail and crash the application. * * <p>Using a separate thread pool prevents image loading from causing other tasks to fail. */ private static final Executor IMAGE_THREAD_POOL_EXECUTOR; static { - ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, - MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, - sThreadFactory); + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAXIMUM_POOL_SIZE, + KEEP_ALIVE_SECONDS, + TimeUnit.SECONDS, + sPoolWorkQueue, + sThreadFactory); threadPoolExecutor.allowCoreThreadTimeOut(true); IMAGE_THREAD_POOL_EXECUTOR = threadPoolExecutor; } @@ -91,28 +94,26 @@ public final class ImageLoader { /** * Handles when image loading is finished. * - * <p>Use this to prevent leaking an Activity or other Context while image loading is - * still pending. When you extend this class you <strong>MUST NOT</strong> use a non static - * inner class, or the containing object will still be leaked. + * <p>Use this to prevent leaking an Activity or other Context while image loading is still + * pending. When you extend this class you <strong>MUST NOT</strong> use a non static inner + * class, or the containing object will still be leaked. */ @UiThread - public static abstract class ImageLoaderCallback<T> { + public abstract static class ImageLoaderCallback<T> { private final WeakReference<T> mWeakReference; /** * Creates an callback keeping a weak reference to {@code referent}. * - * <p> If the "referent" is no longer valid, it no longer makes sense to run the - * callback. The referent is the View, or Activity or whatever that actually needs to - * receive the Bitmap. If the referent has been GC, then no need to run the callback. + * <p>If the "referent" is no longer valid, it no longer makes sense to run the callback. + * The referent is the View, or Activity or whatever that actually needs to receive the + * Bitmap. If the referent has been GC, then no need to run the callback. */ public ImageLoaderCallback(T referent) { mWeakReference = new WeakReference<>(referent); } - /** - * Called when bitmap is loaded. - */ + /** Called when bitmap is loaded. */ private void onBitmapLoaded(@Nullable Bitmap bitmap) { T referent = mWeakReference.get(); if (referent != null) { @@ -122,9 +123,7 @@ public final class ImageLoader { } } - /** - * Called when bitmap is loaded if the weak reference is still valid. - */ + /** Called when bitmap is loaded if the weak reference is still valid. */ public abstract void onBitmapLoaded(T referent, @Nullable Bitmap bitmap); } @@ -134,62 +133,80 @@ public final class ImageLoader { * Preload a bitmap image into the cache. * * <p>Not to make heavy CPU load, AsyncTask.SERIAL_EXECUTOR is used for the image loading. + * * <p>This method is thread safe. */ - public static void prefetchBitmap(Context context, final String uriString, final int maxWidth, - final int maxHeight) { + public static void prefetchBitmap( + Context context, final String uriString, final int maxWidth, final int maxHeight) { if (DEBUG) Log.d(TAG, "prefetchBitmap() " + uriString); if (Looper.getMainLooper() == Looper.myLooper()) { doLoadBitmap(context, uriString, maxWidth, maxHeight, null, AsyncTask.SERIAL_EXECUTOR); } else { final Context appContext = context.getApplicationContext(); - getMainHandler().post(new Runnable() { - @Override - @MainThread - public void run() { - // Calling from the main thread prevents a ConcurrentModificationException - // in LoadBitmapTask.onPostExecute - doLoadBitmap(appContext, uriString, maxWidth, maxHeight, null, - AsyncTask.SERIAL_EXECUTOR); - } - }); + getMainHandler() + .post( + new Runnable() { + @Override + @MainThread + public void run() { + // Calling from the main thread prevents a + // ConcurrentModificationException + // in LoadBitmapTask.onPostExecute + doLoadBitmap( + appContext, + uriString, + maxWidth, + maxHeight, + null, + AsyncTask.SERIAL_EXECUTOR); + } + }); } } /** * Load a bitmap image with the cache using a ContentResolver. * - * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in - * the cache. + * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in the + * cache. * * @return {@code true} if the load is complete and the callback is executed. */ @UiThread - public static boolean loadBitmap(Context context, String uriString, - ImageLoaderCallback callback) { + public static boolean loadBitmap( + Context context, String uriString, ImageLoaderCallback callback) { return loadBitmap(context, uriString, Integer.MAX_VALUE, Integer.MAX_VALUE, callback); } /** * Load a bitmap image with the cache and resize it with given params. * - * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in - * the cache. + * <p><b>Note</b> that the callback will be called synchronously if the bitmap already is in the + * cache. * * @return {@code true} if the load is complete and the callback is executed. */ @UiThread - public static boolean loadBitmap(Context context, String uriString, int maxWidth, int maxHeight, + public static boolean loadBitmap( + Context context, + String uriString, + int maxWidth, + int maxHeight, ImageLoaderCallback callback) { if (DEBUG) { Log.d(TAG, "loadBitmap() " + uriString); } - return doLoadBitmap(context, uriString, maxWidth, maxHeight, callback, - IMAGE_THREAD_POOL_EXECUTOR); + return doLoadBitmap( + context, uriString, maxWidth, maxHeight, callback, IMAGE_THREAD_POOL_EXECUTOR); } - private static boolean doLoadBitmap(Context context, String uriString, - int maxWidth, int maxHeight, ImageLoaderCallback callback, Executor executor) { + private static boolean doLoadBitmap( + Context context, + String uriString, + int maxWidth, + int maxHeight, + ImageLoaderCallback callback, + Executor executor) { // Check the cache before creating a Task. The cache will be checked again in doLoadBitmap // but checking a cache is much cheaper than creating an new task. ImageCache imageCache = ImageCache.getInstance(); @@ -200,7 +217,9 @@ public final class ImageLoader { } return true; } - return doLoadBitmap(callback, executor, + return doLoadBitmap( + callback, + executor, new LoadBitmapFromUriTask(context, imageCache, uriString, maxWidth, maxHeight)); } @@ -219,12 +238,10 @@ public final class ImageLoader { return doLoadBitmap(callback, IMAGE_THREAD_POOL_EXECUTOR, loadBitmapTask); } - /** - * @return {@code true} if the load is complete and the callback is executed. - */ + /** @return {@code true} if the load is complete and the callback is executed. */ @UiThread - private static boolean doLoadBitmap(ImageLoaderCallback callback, Executor executor, - LoadBitmapTask loadBitmapTask) { + private static boolean doLoadBitmap( + ImageLoaderCallback callback, Executor executor, LoadBitmapTask loadBitmapTask) { ScaledBitmapInfo bitmapInfo = loadBitmapTask.getFromCache(); boolean needToReload = loadBitmapTask.isReloadNeeded(); if (bitmapInfo != null && !needToReload) { @@ -259,7 +276,7 @@ public final class ImageLoader { * * <p>Implement {@link #doGetBitmapInBackground} to do the actual loading. */ - public static abstract class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> { + public abstract static class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> { protected final Context mAppContext; protected final int mMaxWidth; protected final int mMaxHeight; @@ -273,24 +290,28 @@ public final class ImageLoader { */ private boolean isReloadNeeded() { ScaledBitmapInfo bitmapInfo = getFromCache(); - boolean needToReload = bitmapInfo != null && bitmapInfo - .needToReload(mMaxWidth, mMaxHeight); + boolean needToReload = + bitmapInfo != null && bitmapInfo.needToReload(mMaxWidth, mMaxHeight); if (DEBUG) { if (needToReload) { - Log.d(TAG, "Bitmap needs to be reloaded. {" - + "originalWidth=" + bitmapInfo.bitmap.getWidth() - + ", originalHeight=" + bitmapInfo.bitmap.getHeight() - + ", reqWidth=" + mMaxWidth - + ", reqHeight=" + mMaxHeight - + "}"); + Log.d( + TAG, + "Bitmap needs to be reloaded. {" + + "originalWidth=" + + bitmapInfo.bitmap.getWidth() + + ", originalHeight=" + + bitmapInfo.bitmap.getHeight() + + ", reqWidth=" + + mMaxWidth + + ", reqHeight=" + + mMaxHeight + + "}"); } } return needToReload; } - /** - * Checks if a reload would be needed if the results of other was available. - */ + /** Checks if a reload would be needed if the results of other was available. */ private boolean isReloadNeeded(LoadBitmapTask other) { return (other.mMaxHeight != Integer.MAX_VALUE && mMaxHeight >= other.mMaxHeight * 2) || (other.mMaxWidth != Integer.MAX_VALUE && mMaxWidth >= other.mMaxWidth * 2); @@ -301,11 +322,14 @@ public final class ImageLoader { return mImageCache.get(mKey); } - public LoadBitmapTask(Context context, ImageCache imageCache, String key, int maxHeight, - int maxWidth) { + public LoadBitmapTask( + Context context, ImageCache imageCache, String key, int maxHeight, int maxWidth) { if (maxWidth == 0 || maxHeight == 0) { throw new IllegalArgumentException( - "Image size should not be 0. {width=" + maxWidth + ", height=" + maxHeight + "Image size should not be 0. {width=" + + maxWidth + + ", height=" + + maxHeight + "}"); } mAppContext = context.getApplicationContext(); @@ -315,9 +339,7 @@ public final class ImageLoader { mMaxWidth = maxWidth; } - /** - * Loads the bitmap returning a possibly scaled down version. - */ + /** Loads the bitmap returning a possibly scaled down version. */ @Nullable @WorkerThread public abstract ScaledBitmapInfo doGetBitmapInBackground(); @@ -352,40 +374,48 @@ public final class ImageLoader { @Override public String toString() { - return this.getClass().getSimpleName() + "(" + mKey + " " + mMaxWidth + "x" + mMaxHeight + return this.getClass().getSimpleName() + + "(" + + mKey + + " " + + mMaxWidth + + "x" + + mMaxHeight + ")"; } } private static final class LoadBitmapFromUriTask extends LoadBitmapTask { - private LoadBitmapFromUriTask(Context context, ImageCache imageCache, String uriString, - int maxWidth, int maxHeight) { + private LoadBitmapFromUriTask( + Context context, + ImageCache imageCache, + String uriString, + int maxWidth, + int maxHeight) { super(context, imageCache, uriString, maxHeight, maxWidth); } @Override @Nullable public final ScaledBitmapInfo doGetBitmapInBackground() { - return BitmapUtils - .decodeSampledBitmapFromUriString(mAppContext, getKey(), mMaxWidth, mMaxHeight); + return BitmapUtils.decodeSampledBitmapFromUriString( + mAppContext, getKey(), mMaxWidth, mMaxHeight); } } - /** - * Loads and caches the logo for a given {@link TvInputInfo} - */ + /** Loads and caches the logo for a given {@link TvInputInfo} */ public static final class LoadTvInputLogoTask extends LoadBitmapTask { private final TvInputInfo mInfo; public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) { - super(context, + super( + context, cache, getTvInputLogoKey(info.getId()), context.getResources() .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size), context.getResources() - .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size) - ); + .getDimensionPixelSize(R.dimen.channel_banner_input_logo_size)); mInfo = info; } @@ -403,9 +433,7 @@ public final class ImageLoader { return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight); } - /** - * Returns key of TV input logo. - */ + /** Returns key of TV input logo. */ public static String getTvInputLogoKey(String inputId) { return inputId + "-logo"; } @@ -418,6 +446,5 @@ public final class ImageLoader { return sMainHandler; } - private ImageLoader() { - } + private ImageLoader() {} } |