diff options
author | Nick Chalko <nchalko@google.com> | 2016-05-04 11:20:31 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2016-05-04 11:21:28 -0700 |
commit | 2e1279b8bbe0603fb4399b25b73121bed5953c46 (patch) | |
tree | 83d9dc7e66f196f2da6fb691d5bba5b2ee2b67b9 /common/src | |
parent | adcc7b8a20af38d03a47f8b7c4ab5eed256f085c (diff) | |
download | TV-2e1279b8bbe0603fb4399b25b73121bed5953c46.tar.gz |
Sync to joey ub-tv-dev at e7fbaa585b1eb7afec05f05032d2e8d99fb595d4
Change-Id: Ib2da547fc0b23c3b504e2fac9c635954fc03060f
Diffstat (limited to 'common/src')
14 files changed, 976 insertions, 1967 deletions
diff --git a/common/src/com/android/tv/common/BuildConfig.java b/common/src/com/android/tv/common/BuildConfig.java deleted file mode 100644 index 635903c3..00000000 --- a/common/src/com/android/tv/common/BuildConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * TODO: It's manually generated file to fix build breakage. - * It should be automatically generated. - */ -package com.android.tv.common; -public final class BuildConfig { - public static final boolean DEBUG = false; - public static final boolean ENG = false; - private BuildConfig() {} -} diff --git a/common/src/com/android/tv/common/CollectionUtils.java b/common/src/com/android/tv/common/CollectionUtils.java index 4a7a81f2..f81e51a5 100644 --- a/common/src/com/android/tv/common/CollectionUtils.java +++ b/common/src/com/android/tv/common/CollectionUtils.java @@ -16,45 +16,12 @@ package com.android.tv.common; -import android.os.Build; -import android.util.ArrayMap; -import android.util.ArraySet; - import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; /** * Static utilities for collections */ public class CollectionUtils { - /** - * Returns a new Set suitable for small data sets. - * - * <p>In M and above this is a {@link ArraySet} otherwise it is a {@link HashSet}. - */ - public static <T> Set<T> createSmallSet() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return new ArraySet<>(); - } else { - return new HashSet<>(); - } - } - - /** - * Returns a new Map suitable for small data sets. - * - * <p>In M and above this is a {@link ArrayMap} otherwise it is a {@link HashMap}. - */ - public static <K, V> Map<K, V> createSmallMap() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return new ArrayMap<>(); - } else { - return new HashMap<>(); - } - } /** * Returns an array with the arrays concatenated together. diff --git a/common/src/com/android/tv/common/SoftPreconditions.java b/common/src/com/android/tv/common/SoftPreconditions.java new file mode 100644 index 00000000..9b7713f6 --- /dev/null +++ b/common/src/com/android/tv/common/SoftPreconditions.java @@ -0,0 +1,164 @@ +/* + * 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.common; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.common.BuildConfig; +import com.android.tv.common.feature.Feature; + +/** + * Simple static methods to be called at the start of your own methods to verify + * correct arguments and state. + * + * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and + * logs a warning when it is false. + * + * <p>This is based on com.android.internal.util.Preconditions. + */ +public final class SoftPreconditions { + private static final String TAG = "SoftPreconditions"; + + /** + * Throws or logs if an expression involving the parameter of the calling + * method is not true. + * + * @param expression a boolean expression + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @throws IllegalArgumentException if {@code expression} is true + */ + public static void checkArgument(final boolean expression, String tag, String msg) { + if (!expression) { + warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg)); + } + } + + /** + * Throws or logs if an expression involving the parameter of the calling + * method is not true. + * + * @param expression a boolean expression + * @throws IllegalArgumentException if {@code expression} is true + */ + public static void checkArgument(final boolean expression) { + checkArgument(expression, null, null); + } + + /** + * Throws or logs if an and object is null. + * + * @param reference an object reference + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @return true if the object is null + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(final T reference, String tag, String msg) { + if (reference == null) { + warn(tag, "Null Pointer", msg, new NullPointerException(msg)); + } + return reference; + } + + /** + * Throws or logs if an and object is null. + * + * @param reference an object reference + * @return true if the object is null + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(final T reference) { + return checkNotNull(reference, null, null); + } + + /** + * Throws or logs if an expression involving the state of the calling + * instance, but not involving any parameters to the calling method is not true. + * + * @param expression a boolean expression + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @throws IllegalStateException if {@code expression} is true + */ + public static void checkState(final boolean expression, String tag, String msg) { + if (!expression) { + warn(tag, "Illegal State", msg, new IllegalStateException(msg)); + } + } + + /** + * Throws or logs if an expression involving the state of the calling + * instance, but not involving any parameters to the calling method is not true. + * + * @param expression a boolean expression + * @throws IllegalStateException if {@code expression} is true + */ + public static void checkState(final boolean expression) { + checkState(expression, null, null); + } + + /** + * Throws or logs if the Feature is not enabled + * + * @param context an android context + * @param feature the required feature + * @param tag used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs + * @throws IllegalStateException if {@code feature} is not enabled + */ + public static void checkFeatureEnabled(Context context, Feature feature, String tag) { + checkState(feature.isEnabled(context), tag, feature.toString()); + } + + /** + * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged + * @param e The exception to wrap with a RuntimeException when thrown. + */ + public static void warn(String tag, String prefix, String msg, Exception e) + throws RuntimeException { + if (TextUtils.isEmpty(tag)) { + tag = TAG; + } + String logMessage; + if (TextUtils.isEmpty(msg)) { + logMessage = prefix; + } else if (TextUtils.isEmpty(prefix)) { + logMessage = msg; + } else { + logMessage = prefix + ": " + msg; + } + + if (BuildConfig.ENG) { + throw new RuntimeException(msg, e); + } else { + Log.w(tag, logMessage, e); + } + } + + private SoftPreconditions() { + } +} diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java index 5ca780e3..7ea86287 100644 --- a/common/src/com/android/tv/common/TvContentRatingCache.java +++ b/common/src/com/android/tv/common/TvContentRatingCache.java @@ -20,6 +20,7 @@ import android.media.tv.TvContentRating; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import java.util.ArrayList; @@ -42,8 +43,7 @@ public final class TvContentRatingCache implements MemoryManageable { return INSTANCE; } - private final Map<String, TvContentRating[]> mRatingsMultiMap = CollectionUtils - .createSmallMap(); + private final Map<String, TvContentRating[]> mRatingsMultiMap = new ArrayMap<>(); /** * Returns an array TvContentRatings from a string of comma separated set of rating strings diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java index bfef19a6..9925833f 100644 --- a/common/src/com/android/tv/common/feature/CommonFeatures.java +++ b/common/src/com/android/tv/common/feature/CommonFeatures.java @@ -17,7 +17,7 @@ package com.android.tv.common.feature; 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.OR; import static com.android.tv.common.feature.TestableFeature.createTestableFeature; /** @@ -32,9 +32,12 @@ public class CommonFeatures { * <p>See <a href="https://goto.google.com/atv-dvr-onepager">go/atv-dvr-onepager</a> */ public static TestableFeature DVR = createTestableFeature( - AND( - ENG_ONLY_FEATURE, - new PropertyFeature("dvr_enabled", false), - Sdk.M_FEATURE // TODO(dvr): Sdk.N_PREVIEW_FEATURE - )); + OR(ENG_ONLY_FEATURE, Sdk.N_PRE_2_OR_HIGHER)); + + /** + * USE_SW_CODEC_FOR_SD + * + * Prefer software based codec for SD channels. + */ + public static Feature USE_SW_CODEC_FOR_SD = new PropertyFeature("use_sw_codec_for_sd", true); } diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java index 1efefd89..268eaea7 100644 --- a/common/src/com/android/tv/common/feature/Sdk.java +++ b/common/src/com/android/tv/common/feature/Sdk.java @@ -18,25 +18,50 @@ package com.android.tv.common.feature; import android.content.Context; import android.os.Build; +import android.support.v4.os.BuildCompat; /** * Holder for SDK version features */ public class Sdk { - public static Feature M_FEATURE = new SdkVersionFeature(Build.VERSION_CODES.M); - private static class SdkVersionFeature implements Feature { + public static Feature N_PRE_2_OR_HIGHER = + new SdkPreviewVersionFeature(Build.VERSION_CODES.M, 2, true); + + private static class SdkPreviewVersionFeature implements Feature { private final int mVersionCode; + private final int mPreviewCode; + private final boolean mAllowHigherPreview; - private SdkVersionFeature(int versionCode) { + private SdkPreviewVersionFeature(int versionCode, int previewCode, + boolean allowHigerPreview) { mVersionCode = versionCode; + mPreviewCode = previewCode; + mAllowHigherPreview = allowHigerPreview; } @Override public boolean isEnabled(Context context) { - return Build.VERSION.SDK_INT >= mVersionCode; + try { + if (mAllowHigherPreview) { + return Build.VERSION.SDK_INT == mVersionCode + && Build.VERSION.PREVIEW_SDK_INT >= mPreviewCode; + } else { + return Build.VERSION.SDK_INT == mVersionCode + && Build.VERSION.PREVIEW_SDK_INT == mPreviewCode; + } + } catch (NoSuchFieldError e) { + return false; + } } } + public static Feature AT_LEAST_N = new Feature() { + @Override + public boolean isEnabled(Context context) { + return BuildCompat.isAtLeastN(); + } + }; + private Sdk() {} } diff --git a/common/src/com/android/tv/common/recording/PlaybackTvView.java b/common/src/com/android/tv/common/recording/PlaybackTvView.java deleted file mode 100644 index e62ee06f..00000000 --- a/common/src/com/android/tv/common/recording/PlaybackTvView.java +++ /dev/null @@ -1,191 +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.common.recording; - -import android.annotation.TargetApi; -import android.content.Context; -import android.media.tv.TvContentRating; -import android.media.tv.TvContract; -import android.media.tv.TvTrackInfo; -import android.media.tv.TvView; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.AttributeSet; - -import com.android.tv.common.feature.CommonFeatures; - -import java.util.List; - -/** - * Extend {@link TvView} to support recording playback. - */ -@TargetApi(Build.VERSION_CODES.M) // TODO(DVR): set to N -public class PlaybackTvView extends TvView { - - final TvInputCallback mInternalCallback = new TvInputCallback() { - @Override - public void onChannelRetuned(String inputId, Uri channelUri) { - if (mCallback != null) { - mCallback.onChannelRetuned(inputId, channelUri); - } - } - - @Override - public void onConnectionFailed(String inputId) { - if (mCallback != null) { - mCallback.onConnectionFailed(inputId); - } - } - - @Override - public void onContentAllowed(String inputId) { - if (mCallback != null) { - mCallback.onContentAllowed(inputId); - } - } - - @Override - public void onContentBlocked(String inputId, TvContentRating rating) { - if (mCallback != null) { - mCallback.onContentBlocked(inputId, rating); - } - } - - @Override - public void onDisconnected(String inputId) { - if (mCallback != null) { - mCallback.onDisconnected(inputId); - } - } - - @Override - public void onEvent(String inputId, String eventType, Bundle eventArgs) { - if (mCallback != null) { - if (eventType.equals(RecordingUtils.EVENT_TYPE_TIMESHIFT_END_POSITION)) { - if (mTimeshiftCallback != null) { - mTimeshiftCallback.onTimeShiftEndPositionChanged(inputId, - eventArgs.getLong(RecordingUtils.BUNDLE_TIMESHIFT_END_POSITION)); - } - return; - } - mCallback.onEvent(inputId, eventType, eventArgs); - } - } - - @Override - public void onTimeShiftStatusChanged(String inputId, int status) { - if (mCallback != null) { - mCallback.onTimeShiftStatusChanged(inputId, status); - } - } - - @Override - public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { - if (mCallback != null) { - mCallback.onTracksChanged(inputId, tracks); - } - } - - @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (mCallback != null) { - mCallback.onTrackSelected(inputId, type, trackId); - } - } - - @Override - public void onVideoAvailable(String inputId) { - if (mCallback != null) { - mCallback.onVideoAvailable(inputId); - } - } - - @Override - public void onVideoSizeChanged(String inputId, int width, int height) { - if (mCallback != null) { - mCallback.onVideoSizeChanged(inputId, width, height); - } - } - - @Override - public void onVideoUnavailable(String inputId, int reason) { - if (mCallback != null) { - mCallback.onVideoUnavailable(inputId, reason); - } - } - }; - - private TvInputCallback mCallback; - private TimeShiftPositionCallback2 mTimeshiftCallback; - - public PlaybackTvView(Context context) { - this(context, null, 0); - } - - public PlaybackTvView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PlaybackTvView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - /** - * Start playback of recording. Once TvInput is ready to play, onVideoAvailable will be called. - * Playback control will be done with timeshift method for seek, play, pause. - */ - public void playMedia(String inputId, Uri mediaUri) { - tune(inputId, TvContract.buildChannelUri(0), RecordingUtils.buildMediaUri(mediaUri)); - } - - @Override - public void tune(String inputId, Uri channelUri, Bundle params) { - super.tune(inputId, channelUri, params); - if (CommonFeatures.DVR.isEnabled(getContext())) { - sendAppPrivateCommand(RecordingUtils.APP_PRIV_CREATE_PLAYBACK_SESSION, null); - } - } - - public void setTimeShiftPositionCallback(TimeShiftPositionCallback2 callback) { - if (CommonFeatures.DVR.isEnabled(getContext())) { - mTimeshiftCallback = callback; - } - super.setTimeShiftPositionCallback(callback); - } - - @Override - public void setCallback(TvInputCallback callback) { - if (CommonFeatures.DVR.isEnabled(getContext())) { - mCallback = callback; - if (callback == null) { - super.setCallback(null); - } else { - super.setCallback(mInternalCallback); - } - } else { - super.setCallback(callback); - } - } - - /** - * We need end position for recording playback. - */ - public abstract static class TimeShiftPositionCallback2 extends TimeShiftPositionCallback { - public void onTimeShiftEndPositionChanged(String inputId, long timeMs) { } - } -} diff --git a/common/src/com/android/tv/common/recording/RecordedProgram.java b/common/src/com/android/tv/common/recording/RecordedProgram.java new file mode 100644 index 00000000..63ce6ff9 --- /dev/null +++ b/common/src/com/android/tv/common/recording/RecordedProgram.java @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.common.recording; + +import static android.media.tv.TvContract.RecordedPrograms; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.common.R; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Objects; + +/** + * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. + */ +public class RecordedProgram { + 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_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_DATA, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + RecordedPrograms.COLUMN_VERSION_NUMBER, + }; + + public static final RecordedProgram fromCursor(Cursor cursor) { + int index = 0; + return builder() + .setId(cursor.getLong(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++)) + .setContentRating(cursor.getString(index++)) + .setPosterArt(cursor.getString(index++)) + .setThumbnail(cursor.getString(index++)) + .setSearchable(cursor.getInt(index++) == 1) + .setDataUri(cursor.getString(index++)) + .setDataBytes(cursor.getLong(index++)) + .setDurationMillis(cursor.getLong(index++)) + .setExpireTimeUtcMillis(cursor.getLong(index++)) + .setInternalProviderData(cursor.getBlob(index++)) + .setInternalProviderFlag1(cursor.getInt(index++)) + .setInternalProviderFlag2(cursor.getInt(index++)) + .setInternalProviderFlag3(cursor.getInt(index++)) + .setInternalProviderFlag4(cursor.getInt(index++)) + .setVersionNumber(cursor.getInt(index++)) + .build(); + } + + public static ContentValues toValues(RecordedProgram recordedProgram) { + ContentValues values = new ContentValues(); + if (recordedProgram.mId != ID_NOT_SET) { + values.put(RecordedPrograms._ID, recordedProgram.mId); + } + values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId); + values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId); + values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle); + values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber); + values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle); + values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber); + values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle); + values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + recordedProgram.mStartTimeUtcMillis); + values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis); + values.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, + safeEncode(recordedProgram.mBroadcastGenres)); + values.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, + safeEncode(recordedProgram.mCanonicalGenres)); + values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription); + values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription); + if (recordedProgram.mVideoWidth == 0) { + values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH); + } else { + values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth); + } + if (recordedProgram.mVideoHeight == 0) { + values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT); + } else { + values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); + } + values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); + values.put(RecordedPrograms.COLUMN_CONTENT_RATING, recordedProgram.mContentRating); + values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, + safeToString(recordedProgram.mPosterArt)); + values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, safeToString(recordedProgram.mThumbnail)); + 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_BYTES, recordedProgram.mDataBytes); + 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, + recordedProgram.mInternalProviderData); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + recordedProgram.mInternalProviderFlag1); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + recordedProgram.mInternalProviderFlag2); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + recordedProgram.mInternalProviderFlag3); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + recordedProgram.mInternalProviderFlag4); + values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber); + return values; + } + + public static class Builder{ + private long mId = ID_NOT_SET; + private String mInputId; + private long mChannelId; + private String mTitle; + private String mSeasonNumber; + private String mSeasonTitle; + private String mEpisodeNumber; + private String mEpisodeTitle; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private String[] mBroadcastGenres; + private String[] mCanonicalGenres; + private String mShortDescription; + private String mLongDescription; + private int mVideoWidth; + private int mVideoHeight; + private String mAudioLanguage; + private String mContentRating; + private Uri mPosterArt; + private Uri mThumbnail; + private boolean mSearchable = true; + private Uri mDataUri; + private long mDataBytes; + private long mDurationMillis; + private long mExpireTimeUtcMillis; + private byte[] mInternalProviderData; + private int mInternalProviderFlag1; + private int mInternalProviderFlag2; + private int mInternalProviderFlag3; + private int mInternalProviderFlag4; + private int mVersionNumber; + + public Builder setId(long id) { + mId = id; + return this; + } + + public Builder setInputId(String inputId) { + mInputId = inputId; + return this; + } + + public Builder setChannelId(long channelId) { + mChannelId = channelId; + return this; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setSeasonNumber(String seasonNumber) { + mSeasonNumber = seasonNumber; + return this; + } + + public Builder setSeasonTitle(String seasonTitle) { + mSeasonTitle = seasonTitle; + return this; + } + + public Builder setEpisodeNumber(String episodeNumber) { + mEpisodeNumber = episodeNumber; + return this; + } + + public Builder setEpisodeTitle(String episodeTitle) { + mEpisodeTitle = episodeTitle; + return this; + } + + public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { + mStartTimeUtcMillis = startTimeUtcMillis; + return this; + } + + public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { + mEndTimeUtcMillis = endTimeUtcMillis; + return this; + } + + public Builder setBroadcastGenres(String broadcastGenres) { + if (TextUtils.isEmpty(broadcastGenres)) { + mBroadcastGenres = null; + return this; + } + return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres)); + } + + private Builder setBroadcastGenres(String[] broadcastGenres) { + mBroadcastGenres = broadcastGenres; + return this; + } + + public Builder setCanonicalGenres(String canonicalGenres) { + if (TextUtils.isEmpty(canonicalGenres)) { + mCanonicalGenres = null; + return this; + } + return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres)); + } + + private Builder setCanonicalGenres(String[] canonicalGenres) { + mCanonicalGenres = canonicalGenres; + return this; + } + + public Builder setShortDescription(String shortDescription) { + mShortDescription = shortDescription; + return this; + } + + public Builder setLongDescription(String longDescription) { + mLongDescription = longDescription; + return this; + } + + public Builder setVideoWidth(int videoWidth) { + mVideoWidth = videoWidth; + return this; + } + + public Builder setVideoHeight(int videoHeight) { + mVideoHeight = videoHeight; + return this; + } + + public Builder setAudioLanguage(String audioLanguage) { + mAudioLanguage = audioLanguage; + return this; + } + + public Builder setContentRating(String contentRating) { + mContentRating = contentRating; + return this; + } + + private Uri toUri(String uriString) { + try { + return uriString == null ? null : Uri.parse(uriString); + } catch (Exception e) { + return null; + } + } + + public Builder setPosterArt(String posterArtUri) { + return setPosterArt(toUri(posterArtUri)); + } + + public Builder setPosterArt(Uri posterArt) { + mPosterArt = posterArt; + return this; + } + + public Builder setThumbnail(String thumbnailUri) { + return setThumbnail(toUri(thumbnailUri)); + } + + public Builder setThumbnail(Uri thumbnail) { + mThumbnail = thumbnail; + return this; + } + + public Builder setSearchable(boolean searchable) { + mSearchable = searchable; + return this; + } + + public Builder setDataUri(String dataUri) { + return setDataUri(toUri(dataUri)); + } + + public Builder setDataUri(Uri dataUri) { + mDataUri = dataUri; + return this; + } + + public Builder setDataBytes(long dataBytes) { + mDataBytes = dataBytes; + return this; + } + + public Builder setDurationMillis(long durationMillis) { + mDurationMillis = durationMillis; + return this; + } + + public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) { + mExpireTimeUtcMillis = expireTimeUtcMillis; + return this; + } + + public Builder setInternalProviderData(byte[] internalProviderData) { + mInternalProviderData = internalProviderData; + return this; + } + + public Builder setInternalProviderFlag1(int internalProviderFlag1) { + mInternalProviderFlag1 = internalProviderFlag1; + return this; + } + + public Builder setInternalProviderFlag2(int internalProviderFlag2) { + mInternalProviderFlag2 = internalProviderFlag2; + return this; + } + + public Builder setInternalProviderFlag3(int internalProviderFlag3) { + mInternalProviderFlag3 = internalProviderFlag3; + return this; + } + + public Builder setInternalProviderFlag4(int internalProviderFlag4) { + mInternalProviderFlag4 = internalProviderFlag4; + return this; + } + + public Builder setVersionNumber(int versionNumber) { + mVersionNumber = versionNumber; + return this; + } + + public RecordedProgram build() { + return new RecordedProgram(mId, mInputId, mChannelId, mTitle, mSeasonNumber, + mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis, + mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription, + mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRating, + mPosterArt, mThumbnail, mSearchable, mDataUri, mDataBytes, mDurationMillis, + mExpireTimeUtcMillis, mInternalProviderData, mInternalProviderFlag1, + mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4, + mVersionNumber); + } + } + + public static Builder builder() { return new Builder(); } + + public static Builder buildFrom(RecordedProgram orig) { + return builder() + .setId(orig.getId()) + .setInputId(orig.getInputId()) + .setChannelId(orig.getChannelId()) + .setTitle(orig.getTitle()) + .setSeasonNumber(orig.getSeasonNumber()) + .setSeasonTitle(orig.getSeasonTitle()) + .setEpisodeNumber(orig.getEpisodeNumber()) + .setEpisodeTitle(orig.getEpisodeTitle()) + .setStartTimeUtcMillis(orig.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(orig.getEndTimeUtcMillis()) + .setBroadcastGenres(orig.getBroadcastGenres()) + .setCanonicalGenres(orig.getCanonicalGenres()) + .setShortDescription(orig.getShortDescription()) + .setLongDescription(orig.getLongDescription()) + .setVideoWidth(orig.getVideoWidth()) + .setVideoHeight(orig.getVideoHeight()) + .setAudioLanguage(orig.getAudioLanguage()) + .setContentRating(orig.getContentRating()) + .setPosterArt(orig.getPosterArt()) + .setThumbnail(orig.getThumbnail()) + .setSearchable(orig.isSearchable()) + .setInternalProviderData(orig.getInternalProviderData()) + .setInternalProviderFlag1(orig.getInternalProviderFlag1()) + .setInternalProviderFlag2(orig.getInternalProviderFlag2()) + .setInternalProviderFlag3(orig.getInternalProviderFlag3()) + .setInternalProviderFlag4(orig.getInternalProviderFlag4()) + .setVersionNumber(orig.getVersionNumber()); + } + + public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR + = new Comparator<RecordedProgram>() { + @Override + public int compare(RecordedProgram lhs, RecordedProgram rhs) { + int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis()); + if (res != 0) { + return res; + } + return Long.compare(lhs.mId, rhs.mId); + } + }; + + private final long mId; + private final String mInputId; + private final long mChannelId; + private final String mTitle; + private final String mSeasonNumber; + private final String mSeasonTitle; + private final String mEpisodeNumber; + private final String mEpisodeTitle; + private final long mStartTimeUtcMillis; + private final long mEndTimeUtcMillis; + private final String[] mBroadcastGenres; + private final String[] mCanonicalGenres; + private final String mShortDescription; + private final String mLongDescription; + private final int mVideoWidth; + private final int mVideoHeight; + private final String mAudioLanguage; + private final String mContentRating; + private final Uri mPosterArt; + private final Uri mThumbnail; + private final boolean mSearchable; + private final Uri mDataUri; + private final long mDataBytes; + private final long mDurationMillis; + private final long mExpireTimeUtcMillis; + private final byte[] mInternalProviderData; + private final int mInternalProviderFlag1; + private final int mInternalProviderFlag2; + private final int mInternalProviderFlag3; + private final int mInternalProviderFlag4; + private final int mVersionNumber; + + private RecordedProgram(long id, String inputId, long channelId, String title, + 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, String contentRating, + Uri posterArt, Uri thumbnail, boolean searchable, Uri dataUri, long dataBytes, + long durationMillis, long expireTimeUtcMillis, byte[] internalProviderData, + int internalProviderFlag1, int internalProviderFlag2, int internalProviderFlag3, + int internalProviderFlag4, int versionNumber) { + mId = id; + mInputId = inputId; + mChannelId = channelId; + mTitle = title; + mSeasonNumber = seasonNumber; + mSeasonTitle = seasonTitle; + mEpisodeNumber = episodeNumber; + mEpisodeTitle = episodeTitle; + mStartTimeUtcMillis = startTimeUtcMillis; + mEndTimeUtcMillis = endTimeUtcMillis; + mBroadcastGenres = broadcastGenres; + mCanonicalGenres = canonicalGenres; + mShortDescription = shortDescription; + mLongDescription = longDescription; + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + mAudioLanguage = audioLanguage; + mContentRating = contentRating; + mPosterArt = posterArt; + mThumbnail = thumbnail; + mSearchable = searchable; + mDataUri = dataUri; + mDataBytes = dataBytes; + mDurationMillis = durationMillis; + mExpireTimeUtcMillis = expireTimeUtcMillis; + mInternalProviderData = internalProviderData; + mInternalProviderFlag1 = internalProviderFlag1; + mInternalProviderFlag2 = internalProviderFlag2; + mInternalProviderFlag3 = internalProviderFlag3; + mInternalProviderFlag4 = internalProviderFlag4; + mVersionNumber = versionNumber; + } + + public String getAudioLanguage() { + return mAudioLanguage; + } + + public String[] getBroadcastGenres() { + return mBroadcastGenres; + } + + public String[] getCanonicalGenres() { + return mCanonicalGenres; + } + + public long getChannelId() { + return mChannelId; + } + + public String getContentRating() { + return mContentRating; + } + + public Uri getDataUri() { + return mDataUri; + } + + public long getDataBytes() { + return mDataBytes; + } + + public long getDurationMillis() { + return mDurationMillis; + } + + public long getEndTimeUtcMillis() { + return mEndTimeUtcMillis; + } + + public String getEpisodeNumber() { + return mEpisodeNumber; + } + + public String getEpisodeTitle() { + return mEpisodeTitle; + } + + public String getEpisodeDisplayTitle(Context context) { + if (!TextUtils.isEmpty(mSeasonNumber) && !TextUtils.isEmpty(mEpisodeNumber) + && !TextUtils.isEmpty(mEpisodeTitle)) { + return String.format(context.getResources().getString(R.string.episode_format), + mSeasonNumber, mEpisodeNumber, mEpisodeTitle); + } + return mEpisodeTitle; + } + + public long getExpireTimeUtcMillis() { + return mExpireTimeUtcMillis; + } + + public long getId() { + return mId; + } + + public String getInputId() { + return mInputId; + } + + public byte[] getInternalProviderData() { + return mInternalProviderData; + } + + public int getInternalProviderFlag1() { + return mInternalProviderFlag1; + } + + public int getInternalProviderFlag2() { + return mInternalProviderFlag2; + } + + public int getInternalProviderFlag3() { + return mInternalProviderFlag3; + } + + public int getInternalProviderFlag4() { + return mInternalProviderFlag4; + } + + public String getLongDescription() { + return mLongDescription; + } + + public Uri getPosterArt() { + return mPosterArt; + } + + public boolean isSearchable() { + return mSearchable; + } + + public String getSeasonNumber() { + return mSeasonNumber; + } + + public String getSeasonTitle() { + return mSeasonTitle; + } + + public String getShortDescription() { + return mShortDescription; + } + + public long getStartTimeUtcMillis() { + return mStartTimeUtcMillis; + } + + public Uri getThumbnail() { + return mThumbnail; + } + + public String getTitle() { + return mTitle; + } + + public Uri getUri() { + return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId); + } + + public int getVersionNumber() { + return mVersionNumber; + } + + public int getVideoHeight() { + return mVideoHeight; + } + + public int getVideoWidth() { + return mVideoWidth; + } + + /** + * Compares everything except {@link #getInternalProviderData()} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordedProgram that = (RecordedProgram) o; + return Objects.equals(mId, that.mId) && + Objects.equals(mChannelId, that.mChannelId) && + Objects.equals(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) && + Objects.equals(mContentRating, that.mContentRating) && + Objects.equals(mPosterArt, that.mPosterArt) && + Objects.equals(mThumbnail, that.mThumbnail); + } + + /** + * Hashes based on the ID. + */ + @Override + public int hashCode() { + return Objects.hash(mId); + } + + @Override + public String toString() { + return "RecordedProgram" + + "[" + mId + + "]{ mInputId=" + mInputId + + ", mChannelId='" + mChannelId + '\'' + + ", mTitle='" + mTitle + '\'' + + ", 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 + '\'' + + ", mContentRating='" + mContentRating + '\'' + + ", mPosterArt=" + mPosterArt + + ", mThumbnail=" + mThumbnail + + ", mSearchable=" + mSearchable + + ", mDataUri=" + mDataUri + + ", mDataBytes=" + mDataBytes + + ", mDurationMillis=" + mDurationMillis + + ", mExpireTimeUtcMillis=" + mExpireTimeUtcMillis + + ", mInternalProviderData.length=" + + (mInternalProviderData == null ? "null" : mInternalProviderData.length) + + ", mInternalProviderFlag1=" + mInternalProviderFlag1 + + ", mInternalProviderFlag2=" + mInternalProviderFlag2 + + ", mInternalProviderFlag3=" + mInternalProviderFlag3 + + ", mInternalProviderFlag4=" + mInternalProviderFlag4 + + ", mSeasonNumber=" + mSeasonNumber + + ", mSeasonTitle=" + mSeasonTitle + + ", mVersionNumber=" + mVersionNumber + + '}'; + } + + @Nullable + private static String safeToString(@Nullable Object o) { + return o == null ? null : o.toString(); + } + + @Nullable + private static String safeEncode(@Nullable String[] genres) { + return genres == null ? null : TvContract.Programs.Genres.encode(genres); + } +} diff --git a/common/src/com/android/tv/common/recording/RecordingTvInputService.java b/common/src/com/android/tv/common/recording/RecordingTvInputService.java deleted file mode 100644 index 6eea6ae7..00000000 --- a/common/src/com/android/tv/common/recording/RecordingTvInputService.java +++ /dev/null @@ -1,378 +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.common.recording; - -import android.annotation.TargetApi; -import android.content.Context; -import android.media.PlaybackParams; -import android.media.tv.TvContentRating; -import android.media.tv.TvInputService; -import android.media.tv.TvTrackInfo; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.view.MotionEvent; -import android.view.Surface; - -import com.android.tv.common.feature.CommonFeatures; - -import java.util.List; - -/** - * {@link TvInputService} class that supports recording and playback. - */ -@TargetApi(Build.VERSION_CODES.M) // TODO(DVR): set to N -public abstract class RecordingTvInputService extends TvInputService { - private static final String TAG = "DvrTvInputService"; - private static final boolean DEBUG = true; - - @Override - public final Session onCreateSession(String inputId) { - if (CommonFeatures.DVR.isEnabled(this)) { - return new InternalSession(this, inputId); - } else { - return onCreatePlaybackSession(inputId); - } - } - - /** - * Called when {@link com.android.tv.common.recording.DvrSession#connect} is called. - */ - protected TvRecording.RecordingSession onCreateDvrSession(String inputId) { - return null; - } - - protected abstract PlaybackSession onCreatePlaybackSession(String inputId); - - private class InternalSession extends TvInputService.Session { - final String mInputId; - BaseSession mSessionImpl; - - public InternalSession(Context context, String inputId) { - super(context); - mInputId = inputId; - } - - @Override - public void onRelease() { - if (mSessionImpl != null) { - mSessionImpl.onRelease(); - } - } - - @Override - public boolean onSetSurface(Surface surface) { - return mSessionImpl.onSetSurface(surface); - } - - @Override - public void onSetStreamVolume(float volume) { - mSessionImpl.onSetStreamVolume(volume); - } - - @Override - public boolean onTune(Uri channelUri) { - return mSessionImpl.onTune(channelUri); - } - - @Override - public void onAppPrivateCommand(String action, Bundle data) { - if (action.equals(RecordingUtils.APP_PRIV_CREATE_DVR_SESSION)) { - if (mSessionImpl == null) { - mSessionImpl = onCreateDvrSession(mInputId); - if (mSessionImpl != null) { - mSessionImpl.setPassthroughSession(this); - notifySessionEvent(RecordingUtils.EVENT_TYPE_CONNECTED, null); - } - } - } else if (action.equals(RecordingUtils.APP_PRIV_CREATE_PLAYBACK_SESSION)) { - if (mSessionImpl == null) { - mSessionImpl = onCreatePlaybackSession(mInputId); - if (mSessionImpl != null) { - mSessionImpl.setPassthroughSession(this); - } - } - } else { - if (mSessionImpl == null) { - throw new IllegalStateException(); - } - mSessionImpl.onAppPrivateCommand(action, data); - } - } - - @Override - public android.view.View onCreateOverlayView() { - return mSessionImpl.onCreateOverlayView(); - } - - @Override - public boolean onGenericMotionEvent(android.view.MotionEvent event) { - return mSessionImpl.onGenericMotionEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { - return mSessionImpl.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyLongPress(int keyCode, android.view.KeyEvent event) { - return mSessionImpl.onKeyLongPress(keyCode, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int count, android.view.KeyEvent event) { - return mSessionImpl.onKeyMultiple(keyCode, count, event); - } - - @Override - public boolean onKeyUp(int keyCode, android.view.KeyEvent event) { - return mSessionImpl.onKeyUp(keyCode, event); - } - - @Override - public void onOverlayViewSizeChanged(int width, int height) { - mSessionImpl.onOverlayViewSizeChanged(width, height); - } - - @Override - public boolean onSelectTrack(int type, String trackId) { - return mSessionImpl.onSelectTrack(type, trackId); - } - - @Override - public void onSetMain(boolean isMain) { - mSessionImpl.onSetMain(isMain); - } - - @Override - public void onSurfaceChanged(int format, int width, int height) { - mSessionImpl.onSurfaceChanged(format, width, height); - } - - @Override - public long onTimeShiftGetCurrentPosition() { - return mSessionImpl.onTimeShiftGetCurrentPosition(); - } - - @Override - public long onTimeShiftGetStartPosition() { - return mSessionImpl.onTimeShiftGetStartPosition(); - } - - @Override - public void onTimeShiftPause() { - mSessionImpl.onTimeShiftPause(); - } - - @Override - public void onTimeShiftResume() { - mSessionImpl.onTimeShiftResume(); - } - - @Override - public void onTimeShiftSeekTo(long timeMs) { - mSessionImpl.onTimeShiftSeekTo(timeMs); - } - - @Override - public void onTimeShiftSetPlaybackParams(PlaybackParams params) { - mSessionImpl.onTimeShiftSetPlaybackParams(params); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return mSessionImpl.onTouchEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - return mSessionImpl.onTrackballEvent(event); - } - - @Override - public boolean onTune(Uri channelUri, Bundle params) { - return mSessionImpl.onTune(channelUri, params); - } - - @Override - public void onUnblockContent(TvContentRating unblockedRating) { - mSessionImpl.onUnblockContent(unblockedRating); - } - - @Override - public void onSetCaptionEnabled(boolean enabled) { - mSessionImpl.onSetCaptionEnabled(enabled); - } - } - - /** - * Base class for {@link PlaybackSession} and {@link TvRecording.RecordingSession}. Do not use it directly - * outside of this class. - */ - public static abstract class BaseSession extends TvInputService.Session { - private Session mPassthroughSession; - - public BaseSession(Context context) { - super(context); - } - - private void setPassthroughSession(Session passthroughSession) { - mPassthroughSession = passthroughSession; - } - - @Override - public void setOverlayViewEnabled(boolean enable) { - if (mPassthroughSession != null) { - mPassthroughSession.setOverlayViewEnabled(enable); - } else { - super.setOverlayViewEnabled(enable); - } - } - - @Override - public void notifyChannelRetuned(Uri channelUri) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyChannelRetuned(channelUri); - } else { - super.notifyChannelRetuned(channelUri); - } - } - - @Override - public void notifyContentAllowed() { - if (mPassthroughSession != null) { - mPassthroughSession.notifyContentAllowed(); - } else { - super.notifyContentAllowed(); - } - } - - @Override - public void notifyContentBlocked(TvContentRating rating) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyContentBlocked(rating); - } else { - super.notifyContentBlocked(rating); - } - } - - @Override - public void notifySessionEvent(String eventType, Bundle eventArgs) { - if (mPassthroughSession != null) { - mPassthroughSession.notifySessionEvent(eventType, eventArgs); - } else { - super.notifySessionEvent(eventType, eventArgs); - } - } - - @Override - public void notifyTimeShiftStatusChanged(int status) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyTimeShiftStatusChanged(status); - } else { - super.notifyTimeShiftStatusChanged(status); - } - } - - @Override - public void notifyTracksChanged(List<TvTrackInfo> tracks) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyTracksChanged(tracks); - } else { - super.notifyTracksChanged(tracks); - } - } - - @Override - public void notifyTrackSelected(int type, String trackId) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyTrackSelected(type, trackId); - } else { - super.notifyTrackSelected(type, trackId); - } - } - - @Override - public void notifyVideoAvailable() { - if (mPassthroughSession != null) { - mPassthroughSession.notifyVideoAvailable(); - } else { - super.notifyVideoAvailable(); - } - } - - @Override - public void notifyVideoUnavailable(int reason) { - if (mPassthroughSession != null) { - mPassthroughSession.notifyVideoUnavailable(reason); - } else { - super.notifyVideoUnavailable(reason); - } - } - } - - /** - * Session linked to {@link android.media.tv.TvView} to tune to a channel or play an recording. - */ - public static abstract class PlaybackSession extends BaseSession { - private boolean mIsRecordingPlayback; - - public PlaybackSession(Context context) { - super(context); - } - - /** - * Returns {@code true}, if the current playback is for a recording. - */ - public boolean isRecordingPlayback() { - return mIsRecordingPlayback; - } - - /** - * Called when it is requested to play an recording {@code mediaUri}. When playback and - * rendering starts, {@link #notifyVideoAvailable} should be called. - */ - public void onPlayMedia(Uri mediaUri) { } - - /** - * Notifies TimeShift end position. It should have the form like onTimeShiftEndPosition. - * But, it's not trivial to add that in the prototyping. The method is recommended to be - * called inside {@link #onTimeShiftGetStartPosition()}, when a recording is played. - */ - public void notifyTimeShiftEndPosition(long endPosition) { - Bundle params = new Bundle(); - params.putLong(RecordingUtils.BUNDLE_TIMESHIFT_END_POSITION, endPosition); - notifySessionEvent(RecordingUtils.EVENT_TYPE_TIMESHIFT_END_POSITION, params); - } - - @Override - public final boolean onTune(Uri channelUri, Bundle params) { - if (params != null && params.getBoolean(RecordingUtils.BUNDLE_IS_DVR, false)) { - notifySessionEvent(RecordingUtils.EVENT_TYPE_CONNECTED, null); - return true; - } else if (params != null && params.containsKey(RecordingUtils.BUNDLE_MEDIA_URI)) { - mIsRecordingPlayback = true; - onPlayMedia(Uri.parse(params.getString(RecordingUtils.BUNDLE_MEDIA_URI))); - return true; - } else { - mIsRecordingPlayback = false; - return onTune(channelUri); - } - } - } -} diff --git a/common/src/com/android/tv/common/recording/RecordingUtils.java b/common/src/com/android/tv/common/recording/RecordingUtils.java deleted file mode 100644 index ae91659f..00000000 --- a/common/src/com/android/tv/common/recording/RecordingUtils.java +++ /dev/null @@ -1,50 +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.common.recording; - -import android.net.Uri; -import android.os.Bundle; - -public class RecordingUtils { - static final int ACTION_START_RECORD = 10055; - static final int ACTION_STOP_RECORD = 10056; - - static final String EVENT_TYPE_CONNECTED = "event_type_connected"; - static final String EVENT_TYPE_TIMESHIFT_END_POSITION = "event_type_timeshift_end_position"; - - static final String APP_PRIV_CREATE_PLAYBACK_SESSION = "app_priv_create_playback_session"; - static final String APP_PRIV_CREATE_DVR_SESSION = "app_priv_create_dvr_session"; - - // Type: boolean - static final String BUNDLE_IS_DVR = "bundle_is_dvr"; - // Type: String (Uri) - static final String BUNDLE_MEDIA_URI = "bundle_media_uri"; - // Type: String - static final String BUNDLE_CHANNEL_URI = "bundle_channel_uri"; - // Type: long - static final String BUNDLE_TIMESHIFT_END_POSITION = "timeshift_end_position"; - - /** - * Builds a {@link Bundle} with {@code mediaUri}. If the bundle is sent with tune command, - * the {@code mediaUri} will be played. - */ - public static Bundle buildMediaUri(Uri mediaUri) { - Bundle params = new Bundle(); - params.putString(RecordingUtils.BUNDLE_MEDIA_URI, mediaUri.toString()); - return params; - } -} diff --git a/common/src/com/android/tv/common/recording/TvRecording.java b/common/src/com/android/tv/common/recording/TvRecording.java deleted file mode 100644 index 28a611a0..00000000 --- a/common/src/com/android/tv/common/recording/TvRecording.java +++ /dev/null @@ -1,384 +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.common.recording; - -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvView; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.IntDef; -import android.util.Log; -import android.view.Surface; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * API for making TV Recordings. - * This class holds both the API under development and the session app private command magic needed - * to simulate the API. - */ -public final class TvRecording { - private static final String TAG = "TvRecording"; - private static final boolean DEBUG = true; // STOPSHIP(DVR) - - @Retention(RetentionPolicy.SOURCE) - @IntDef({RECORD_STOP_REASON_DISKFULL, RECORD_STOP_REASON_CONFLICT, - RECORD_STOP_REASON_CONNECT_FAILED, RECORD_STOP_REASON_DISCONNECTED, - RECORD_STOP_REASON_UNKNOWN}) - public @interface RecordStopReason { - } - - private static final int FIRST_REASON = 1; - public static final int RECORD_STOP_REASON_DISKFULL = 1; - public static final int RECORD_STOP_REASON_CONFLICT = 2; - public static final int RECORD_STOP_REASON_CONNECT_FAILED = 3; - public static final int RECORD_STOP_REASON_DISCONNECTED = 4; - public static final int RECORD_STOP_REASON_UNKNOWN = 5; - private static final int LAST_REASON = 5; - - public abstract static class ClientCallback { - public void onConnected() { } - - public void onDisconnected() { } - - public void onRecordStarted(Uri mediaUri) { } - - public void onRecordStopped(Uri mediaUri, @RecordStopReason int reason) { } - - public void onRecordDeleted(Uri mediaUri) { } - - public void onRecordDeleteFailed(Uri mediaUri, int reason) { } - - public void onCapabilityReceived(RecordingCapability capability) { } - } - - public interface RecordingClientApi { - void release(); - - void startRecord(Uri channelUri, Uri mediaUri); - - void stopRecord(); - - void delete(Uri mediaUri); - - void getCapability(); - } - - public interface RecordingSessionApi { - /** - * Start recording on {@code channelUri}. - * <p>{@link RecordingSession#notifyRecordStarted(Uri)} should be called as soon as the - * recording is started. - */ - void onStartRecord(Uri channelUri, Uri mediaUri); - - /** - * Called when it stops to record. - */ - void onStopRecord(); - - /** - * Called when it is requested to delete {@code mediaUri}. - */ - void onDelete(Uri mediaUri); - - /** - * Called when the client request {@link RecordingCapability}. - */ - RecordingCapability onGetCapability(); - } - - /////////// - // BELOW IS IMPLEMENTATION DETAILS OFTEN SPECIFIC TO USING APP PRIVATE COMMANDS - ////////// - - private static final String PREFIX = "record_"; - - private static final String APP_PRIV_DELETE = PREFIX + "app_priv_delete"; - private static final String APP_PRIV_GET_CAPABILITY = PREFIX + "app_priv_get_capability"; - private static final String APP_PRIV_START_RECORD = PREFIX + "app_priv_start_record"; - private static final String APP_PRIV_STOP_RECORD = PREFIX + "app_priv_stop_record"; - - private static final String EVENT_TYPE_DELETED = PREFIX + "event_type_deleted"; - private static final String EVENT_TYPE_DELETE_FAILED = PREFIX + "event_type_delete_failed"; - private static final String EVENT_TYPE_CAPABILITY_RECEIVED = PREFIX - + "event_type_capability_received"; - private static final String EVENT_TYPE_RECORD_STARTED = PREFIX + "event_type_record_started"; - private static final String EVENT_TYPE_RECORD_STOPPED = PREFIX + "event_type_record_stopped"; - - // Type: int - private static final String BUNDLE_STOPPED_REASON = PREFIX + "stopped_reason"; - // Type: int - private static final String BUNDLE_DELETE_FAILED_REASON = PREFIX + "delete_failed_reason"; - // Type: RecordingCapability - private static final String BUNDLE_CAPABILITY = PREFIX + "capability"; - - - /** - * Session linked to {@link TvRecordingClient} to record contents. - */ - public static abstract class RecordingSession extends RecordingTvInputService.BaseSession - implements RecordingSessionApi { - private final static String TAG = "RecordingSession"; - - public RecordingSession(Context context) { - super(context); - } - - @Override - public final boolean onTune(Uri channelUri) { - // no-op - return false; - } - - @Override - public final boolean onSetSurface(Surface surface) { - // no-op - return false; - } - - @Override - public final void onSetStreamVolume(float volume) { - // no-op - } - - @Override - public final void onSetCaptionEnabled(boolean enabled) { - // no-op - } - - /** - * Notifies when recording starts. It is an response of {@link #onStartRecord}. - */ - public final void notifyRecordStarted(Uri mediaUri) { - notifySessionEvent(EVENT_TYPE_RECORD_STARTED, RecordingUtils.buildMediaUri(mediaUri)); - } - - /** - * Notifies when recording is unexpectedly stopped. - */ - public final void notifyRecordUnexpectedlyStopped(Uri mediaUri, int reason) { - Bundle params = RecordingUtils.buildMediaUri(mediaUri); - params.putInt(BUNDLE_STOPPED_REASON, reason); - notifySessionEvent(EVENT_TYPE_RECORD_STOPPED, params); - } - - /** - * Notifies when the recording {@code mediaUri} is deleted. - */ - public final void notifyDeleted(Uri mediaUri) { - notifySessionEvent(EVENT_TYPE_DELETED, RecordingUtils.buildMediaUri(mediaUri)); - } - - /** - * Notifies when the deletion of the recording {@code mediaUri} is requested through - * {@link #onDelete} but failed. - */ - public final void notifyDeleteFailed(Uri mediaUri, int reason) { - Bundle params = RecordingUtils.buildMediaUri(mediaUri); - params.putInt(BUNDLE_DELETE_FAILED_REASON, reason); - notifySessionEvent(EVENT_TYPE_DELETE_FAILED, params); - } - - @Override - public final void onAppPrivateCommand(String action, Bundle data) { - if (DEBUG) Log.d(TAG, "onAppPrivateCommand(" + action + ", " + data + ")"); - switch (action) { - case APP_PRIV_GET_CAPABILITY: - RecordingCapability capability = onGetCapability(); - Bundle params = new Bundle(); - params.putParcelable(BUNDLE_CAPABILITY, capability); - notifySessionEvent(EVENT_TYPE_CAPABILITY_RECEIVED, params); - break; - case APP_PRIV_DELETE: - onDelete(Uri.parse(data.getString(RecordingUtils.BUNDLE_CHANNEL_URI))); - break; - case APP_PRIV_START_RECORD: - onStartRecord(Uri.parse(data.getString(RecordingUtils.BUNDLE_CHANNEL_URI)), - Uri.parse(data.getString(RecordingUtils.BUNDLE_MEDIA_URI))); - break; - case APP_PRIV_STOP_RECORD: - onStopRecord(); - break; - } - } - } - - /** - * A session used for recording. - */ - public static class TvRecordingClient implements RecordingClientApi { - private static final String TAG = "DvrSessionClient"; - - private ClientCallback mCallback; - private TvView mTvView; - - public TvRecordingClient(Context context) { - if (DEBUG) { - Log.d(TAG, "creating client"); - } - mTvView = new TvView(context); - } - - /** - * Connects the session to a specific input {@code inputId}. - */ - public void connect(String inputId, ClientCallback callback) { - if (DEBUG) { - Log.d(TAG, "connect " + inputId + " with " + callback); - } - mCallback = callback; - Bundle bundle = new Bundle(); - bundle.putBoolean(RecordingUtils.BUNDLE_IS_DVR, true); - mTvView.tune(inputId, TvContract.buildChannelUri(0), bundle); - mTvView.sendAppPrivateCommand(RecordingUtils.APP_PRIV_CREATE_DVR_SESSION, null); - mTvView.setCallback(new TvView.TvInputCallback() { - @Override - public void onConnectionFailed(String inputId) { - if (mCallback == null) { - return; - } - mCallback.onDisconnected(); - } - - @Override - public void onDisconnected(String inputId) { - if (mCallback == null) { - return; - } - mCallback.onDisconnected(); - } - - @Override - public void onEvent(String inputId, String eventType, Bundle eventArgs) { - if (mCallback == null) { - return; - } - String mediaUriString = eventArgs == null ? null - : eventArgs.getString(RecordingUtils.BUNDLE_MEDIA_URI, null); - Uri mediaUri = mediaUriString == null ? null : Uri.parse(mediaUriString); - switch (eventType) { - case RecordingUtils.EVENT_TYPE_CONNECTED: - mCallback.onConnected(); - break; - case EVENT_TYPE_DELETED: - mCallback.onRecordDeleted(mediaUri); - break; - case EVENT_TYPE_DELETE_FAILED: { - // TODO(DVR) use reasons from API - int reason = eventArgs == null ? 0 - : eventArgs.getInt(BUNDLE_DELETE_FAILED_REASON); - mCallback.onRecordDeleteFailed(mediaUri, reason); - break; - } - case EVENT_TYPE_CAPABILITY_RECEIVED: { - RecordingCapability capability = eventArgs - .getParcelable(BUNDLE_CAPABILITY); - mCallback.onCapabilityReceived(capability); - break; - } - case EVENT_TYPE_RECORD_STARTED: - mCallback.onRecordStarted(mediaUri); - break; - case EVENT_TYPE_RECORD_STOPPED: { - int reason = getRecordStopReason(eventArgs); - mCallback.onRecordStopped(mediaUri, reason); - break; - } - } - } - - // TODO: handle track select. - }); - } - - /** - * Releases the session. - */ - @Override - public void release() { - if (DEBUG) { - Log.d(TAG, "release " + this); - } - mTvView.reset(); - mCallback = null; - } - - /** - * Starts recording. - */ - @Override - public void startRecord(Uri channelUri, Uri mediaUri) { - if (DEBUG) { - Log.d(TAG, "startRecord " + channelUri + ", " + mediaUri); - } - Bundle params = RecordingUtils.buildMediaUri(mediaUri); - params.putString(RecordingUtils.BUNDLE_CHANNEL_URI, channelUri.toString()); - mTvView.sendAppPrivateCommand(APP_PRIV_START_RECORD, params); - } - - /** - * Stops recording. - */ - @Override - public void stopRecord() { - if (DEBUG) { - Log.d(TAG, "stopRecord " + this); - } - mTvView.sendAppPrivateCommand(APP_PRIV_STOP_RECORD, null); - } - - /** - * Deletes a recorded media. - */ - @Override - public void delete(Uri mediaUri) { - mTvView.sendAppPrivateCommand(APP_PRIV_DELETE, - RecordingUtils.buildMediaUri(mediaUri)); - } - - @Override - public void getCapability() { - mTvView.sendAppPrivateCommand(APP_PRIV_GET_CAPABILITY, null); - } - - @Override - public String toString() { - return TvRecordingClient.class.getName() + "{" + "callBack=" + mCallback + "}"; - } - } - - @SuppressWarnings("ResourceType") - @RecordStopReason - private static int getRecordStopReason(Bundle eventArgs) { - if(eventArgs == null) { - if (DEBUG) Log.d(TAG, "Null stop reason"); - return RECORD_STOP_REASON_UNKNOWN; - } - int reason = eventArgs.getInt(BUNDLE_STOPPED_REASON); - if (reason < FIRST_REASON || reason > LAST_REASON) { - if (DEBUG) Log.d(TAG, "Unknown stop reason " + reason); - reason = RECORD_STOP_REASON_UNKNOWN; - } - return reason; - } - - private TvRecording() { - } -} diff --git a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java index 28ab97de..5c57d84d 100644 --- a/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java +++ b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java @@ -223,6 +223,9 @@ public class FadeAndShortSlide extends Visibility { float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, left, startX, endX, APPEAR_INTERPOLATOR, this); + if (slideAnimator == null) { + return null; + } mFade.setInterpolator(APPEAR_INTERPOLATOR); final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues)); @@ -245,9 +248,15 @@ public class FadeAndShortSlide extends Visibility { float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); + if (slideAnimator == null) { // slideAnimator is null if startX == endX + return null; + } + mFade.setInterpolator(DISAPPEAR_INTERPOLATOR); - final AnimatorSet set = new AnimatorSet(); final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues); + if (fadeAnimator == null) { + return null; + } fadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { @@ -255,6 +264,8 @@ public class FadeAndShortSlide extends Visibility { view.setAlpha(0.0f); } }); + + final AnimatorSet set = new AnimatorSet(); set.play(slideAnimator).with(fadeAnimator); Long delay = (Long) startValues.values.get(PROPNAME_DELAY); if (delay != null) { diff --git a/common/src/com/android/tv/common/ui/setup/leanback/OnboardingFragment.java b/common/src/com/android/tv/common/ui/setup/leanback/OnboardingFragment.java deleted file mode 100644 index adbd98c2..00000000 --- a/common/src/com/android/tv/common/ui/setup/leanback/OnboardingFragment.java +++ /dev/null @@ -1,531 +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.common.ui.setup.leanback; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.app.Fragment; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnKeyListener; -import android.view.ViewGroup; -import android.view.ViewTreeObserver.OnPreDrawListener; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.tv.common.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * An OnboardingFragment provides a common and simple way to build onboarding screen for - * applications. - * <p> - * <h3>Building the screen</h3> - * The view structure of onboarding screen is composed of the common parts and custom parts. The - * common parts are composed of title, description and page navigator and the custom parts are - * composed of background, contents and foreground. - * <p> - * To build the screen views, the inherited class should override: - * <ul> - * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same - * size as the screen and the lowest z-order.</li> - * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in - * the content area at the center of the screen.</li> - * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same - * size as the screen and the highest z-order</li> - * </ul> - * <p> - * Each of these methods can return {@code null} if the application doesn't want to provide it. - * <p> - * <h3>Page information</h3> - * The onboarding screen may have several pages which explain the functionality of the application. - * The inherited class should provide the page information by overriding the methods: - * <p> - * <ul> - * <li>{@link #getPageCount} to provide the number of pages.</li> - * <li>{@link #getPageTitle} to provide the title of the page.</li> - * <li>{@link #getPageDescription} to provide the description of the page.</li> - * </ul> - * <p> - * <h3><a name="logoAnimation">Logo Splash Animation</a></h3> - * When onboarding screen appears, the logo splash animation is played by default. The animation - * fades in the logo image, pauses in a few seconds and fades it out. To support this animation with - * its own logo image, the inherited class should override the following method. - * <p> - * <ul> - * <li>{@link #getLogoResourceId()}</li> - * </ul> - * <p> - * <h3>Animation</h3> - * This page has three kinds of animations: - * <p> - * <ul> - * <li><b>Logo splash animation</b> which starts as soon as onboarding screen is shown as described - * in <a href="#logoAnimation">Logo Splash Animation</a>.</li> - * <li><b>Page enter animation</b> which runs just after the logo animation finishes. The - * application can run the animations of their custom views by overriding - * {@link #onStartEnterAnimation}.</li> - * <li><b>Page change animation</b> which runs when the page changes. The pages can move backward or - * forward direction and the application can start the page change animations by overriding - * {@link #onStartPageChangeAnimation}.</li> - * </ul> - * <p> - * <h3>Finishing the screen</h3> - * <p> - * If the user finishes the onboarding screen after navigating all the pages, - * {@link #onFinishFragment} is called. The inherited class can override this method to show another - * fragment or activity, or just remove this fragment. - * - * @hide - */ -abstract public class OnboardingFragment extends Fragment { - private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333; - private static final long START_DELAY_TITLE_MS = 33; - private static final long START_DELAY_DESCRIPTION_MS = 33; - - private static final long HEADER_ANIMATION_DURATION_MS = 417; - private static final long DESCRIPTION_START_DELAY_MS = 33; - private static final long HEADER_APPEAR_DELAY_MS = 500; - private static final int SLIDE_DISTANCE = 60; - - private static int sSlideDistance; - - private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator(); - private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR - = new AccelerateInterpolator(); - - private PagingIndicator mPageIndicator; - private View mStartButton; - private ImageView mLogoView; - private TextView mTitleView; - private TextView mDescriptionView; - - private boolean mEnterTransitionFinished; - private int mCurrentPageIndex; - - private AnimatorSet mAnimator; - - /** - * Called to have the inherited class create its own start animation. The start animation runs - * after logo splash animation ends. - */ - abstract protected void onStartEnterAnimation(); - - private final OnClickListener mOnClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - if (!mEnterTransitionFinished) { - // Do not change page until the enter transition finishes. - return; - } - if (mCurrentPageIndex == getPageCount() - 1) { - onFinishFragment(); - } else { - ++mCurrentPageIndex; - onPageChanged(mCurrentPageIndex - 1); - } - } - }; - - private final OnKeyListener mOnKeyListener = new OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (!mEnterTransitionFinished) { - // Ignore key event until the enter transition finishes. - return keyCode != KeyEvent.KEYCODE_BACK; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - return false; - } - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - if (mCurrentPageIndex == 0) { - return false; - } - // pass through - case KeyEvent.KEYCODE_DPAD_LEFT: - if (mCurrentPageIndex > 0) { - --mCurrentPageIndex; - onPageChanged(mCurrentPageIndex + 1); - } - return true; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (mCurrentPageIndex < getPageCount() - 1) { - ++mCurrentPageIndex; - onPageChanged(mCurrentPageIndex - 1); - } - return true; - } - return false; - } - }; - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, final ViewGroup container, - Bundle savedInstanceState) { - ViewGroup view = (ViewGroup) inflater.inflate(R.layout.lb_onboarding_fragment, container, - false); - mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator); - mPageIndicator.setPageCount(getPageCount()); - mPageIndicator.setOnClickListener(mOnClickListener); - mPageIndicator.setOnKeyListener(mOnKeyListener); - mStartButton = view.findViewById(R.id.button_start); - mStartButton.setOnClickListener(mOnClickListener); - mStartButton.setOnKeyListener(mOnKeyListener); - mLogoView = (ImageView) view.findViewById(R.id.logo); - mLogoView.setImageResource(getLogoResourceId()); - mTitleView = (TextView) view.findViewById(R.id.title); - mTitleView.setText(getPageTitle(0)); - mDescriptionView = (TextView) view.findViewById(R.id.description); - mDescriptionView.setText(getPageDescription(0)); - if (sSlideDistance == 0) { - sSlideDistance = (int) (SLIDE_DISTANCE * getActivity().getResources() - .getDisplayMetrics().scaledDensity); - } - mCurrentPageIndex = 0; - mPageIndicator.onPageSelected(0, false); - view.requestFocus(); - if (getLogoResourceId() != 0) { - container.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - container.getViewTreeObserver().removeOnPreDrawListener(this); - startLogoAnimation(); - return true; - } - }); - } else { - onLogoAnimationFinished(); - } - return view; - } - - private void startLogoAnimation() { - mLogoView.setVisibility(View.VISIBLE); - Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_logo_enter); - Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_logo_exit); - outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS); - AnimatorSet animator = new AnimatorSet(); - animator.playSequentially(inAnimator, outAnimator); - animator.setTarget(mLogoView); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mEnterTransitionFinished = true; - if (getActivity() != null) { - onLogoAnimationFinished(); - onStartEnterAnimation(); - } - } - }); - animator.start(); - } - - private void onLogoAnimationFinished() { - mLogoView.setVisibility(View.GONE); - // Create custom views. - LayoutInflater inflater = LayoutInflater.from(getActivity()); - ViewGroup backgroundContainer = (ViewGroup) getView().findViewById( - R.id.background_container); - View background = onCreateBackgroundView(inflater, backgroundContainer); - if (background != null) { - backgroundContainer.setVisibility(View.VISIBLE); - backgroundContainer.addView(background); - } - ViewGroup contentContainer = (ViewGroup) getView().findViewById(R.id.content_container); - View content = onCreateContentView(inflater, contentContainer); - if (content != null) { - contentContainer.setVisibility(View.VISIBLE); - contentContainer.addView(content); - } - ViewGroup foregroundContainer = (ViewGroup) getView().findViewById( - R.id.foreground_container); - View foreground = onCreateForegroundView(inflater, foregroundContainer); - if (foreground != null) { - foregroundContainer.setVisibility(View.VISIBLE); - foregroundContainer.addView(foreground); - } - // Make views visible which were invisible while logo animation is running. - getView().findViewById(R.id.page_container).setVisibility(View.VISIBLE); - getView().findViewById(R.id.content_container).setVisibility(View.VISIBLE); - - List<Animator> animators = new ArrayList<>(); - Animator animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_page_indicator_enter); - if (getPageCount() <= 1) { - // Start button - mStartButton.setVisibility(View.VISIBLE); - animator.setTarget(mStartButton); - } else { - // Page indicator - mPageIndicator.setVisibility(View.VISIBLE); - animator.setTarget(mPageIndicator); - } - animators.add(animator); - // Header title - View view = getActivity().findViewById(R.id.title); - view.setAlpha(0); - animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_title_enter); - animator.setStartDelay(START_DELAY_TITLE_MS); - animator.setTarget(view); - animators.add(animator); - // Header description - view = getActivity().findViewById(R.id.description); - view.setAlpha(0); - animator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_description_enter); - animator.setStartDelay(START_DELAY_DESCRIPTION_MS); - animator.setTarget(view); - animators.add(animator); - mAnimator = new AnimatorSet(); - mAnimator.playTogether(animators); - mAnimator.start(); - onStartEnterAnimation(); - // Search focus and give the focus to the appropriate child which has become visible. - getView().requestFocus(); - } - - /** - * Returns the page count. - * - * @return The page count. - */ - abstract protected int getPageCount(); - - /** - * Returns the title of the given page. - * - * @param pageIndex The page index. - * - * @return The title of the page. - */ - abstract protected String getPageTitle(int pageIndex); - - /** - * Returns the description of the given page. - * - * @param pageIndex The page index. - * - * @return The description of the page. - */ - abstract protected String getPageDescription(int pageIndex); - - /** - * Returns the index of the current page. - * - * @return The index of the current page. - */ - protected final int getCurrentPageIndex() { - return mCurrentPageIndex; - } - - /** - * Returns the resource ID of the splash logo image. - * - * @return The resource ID of the splash logo image. - */ - abstract protected int getLogoResourceId(); - - /** - * Called to have the inherited class create background view. This is optional and the fragment - * which doesn't have the background view can return {@code null}. This is called inside - * {@link #onCreateView}. - * - * @param inflater The LayoutInflater object that can be used to inflate the views, - * @param container The parent view that the additional views are attached to.The fragment - * should not add the view by itself. - * - * @return The background view for the onboarding screen, or {@code null}. - */ - @Nullable - abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container); - - /** - * Called to have the inherited class create content view. This is optional and the fragment - * which doesn't have the content view can return {@code null}. This is called inside - * {@link #onCreateView}. - * - * <p>The content view would be located at the center of the screen. - * - * @param inflater The LayoutInflater object that can be used to inflate the views, - * @param container The parent view that the additional views are attached to.The fragment - * should not add the view by itself. - * - * @return The content view for the onboarding screen, or {@code null}. - */ - @Nullable - abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container); - - /** - * Called to have the inherited class create foreground view. This is optional and the fragment - * which doesn't need the foreground view can return {@code null}. This is called inside - * {@link #onCreateView}. - * - * <p>This foreground view would have the highest z-order. - * - * @param inflater The LayoutInflater object that can be used to inflate the views, - * @param container The parent view that the additional views are attached to.The fragment - * should not add the view by itself. - * - * @return The foreground view for the onboarding screen, or {@code null}. - */ - @Nullable - abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container); - - /** - * Called when the onboarding flow finishes. - */ - protected void onFinishFragment() { } - - /** - * Called when the page changes. - */ - private void onPageChanged(int previousPage) { - if (mAnimator != null) { - mAnimator.end(); - } - mPageIndicator.onPageSelected(mCurrentPageIndex, true); - - List<Animator> animators = new ArrayList<>(); - // Header animation - Animator fadeAnimator = null; - if (previousPage < getCurrentPageIndex()) { - // sliding to left - animators.add(createAnimator(mTitleView, false, Gravity.START, 0)); - animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START, - DESCRIPTION_START_DELAY_MS)); - animators.add(createAnimator(mTitleView, true, Gravity.END, - HEADER_APPEAR_DELAY_MS)); - animators.add(createAnimator(mDescriptionView, true, Gravity.END, - HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); - } else { - // sliding to right - animators.add(createAnimator(mTitleView, false, Gravity.END, 0)); - animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END, - DESCRIPTION_START_DELAY_MS)); - animators.add(createAnimator(mTitleView, true, Gravity.START, - HEADER_APPEAR_DELAY_MS)); - animators.add(createAnimator(mDescriptionView, true, Gravity.START, - HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); - } - final int currentPageIndex = getCurrentPageIndex(); - fadeAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mTitleView.setText(getPageTitle(currentPageIndex)); - mDescriptionView.setText(getPageDescription(currentPageIndex)); - } - }); - - // Animator for switching between page indicator and button. - if (getCurrentPageIndex() == getPageCount() - 1) { - mStartButton.setVisibility(View.VISIBLE); - Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_page_indicator_fade_out); - navigatorFadeOutAnimator.setTarget(mPageIndicator); - Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_start_button_fade_in); - buttonFadeInAnimator.setTarget(mStartButton); - animators.add(navigatorFadeOutAnimator); - navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mPageIndicator.setVisibility(View.GONE); - } - }); - animators.add(buttonFadeInAnimator); - } else if (previousPage == getPageCount() - 1) { - mPageIndicator.setVisibility(View.VISIBLE); - Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_page_indicator_fade_in); - navigatorFadeInAnimator.setTarget(mPageIndicator); - Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(), - R.animator.lb_onboarding_start_button_fade_out); - buttonFadeOutAnimator.setTarget(mStartButton); - buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mStartButton.setVisibility(View.GONE); - } - }); - mAnimator = new AnimatorSet(); - mAnimator.playTogether(navigatorFadeInAnimator, buttonFadeOutAnimator); - mAnimator.start(); - } - mAnimator = new AnimatorSet(); - mAnimator.playTogether(animators); - mAnimator.start(); - onStartPageChangeAnimation(previousPage); - } - - private Animator createAnimator(View view, boolean fadeIn, int slideDirection, - long startDelay) { - boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - boolean slideRight = (isLtr && slideDirection == Gravity.END) - || (!isLtr && slideDirection == Gravity.START) - || slideDirection == Gravity.RIGHT; - Animator fadeAnimator; - Animator slideAnimator; - if (fadeIn) { - fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f); - slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, - slideRight ? sSlideDistance : -sSlideDistance, 0); - fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); - slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); - } else { - fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f); - slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0, - slideRight ? sSlideDistance : -sSlideDistance); - fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); - slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); - } - fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); - fadeAnimator.setTarget(view); - slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); - slideAnimator.setTarget(view); - AnimatorSet animator = new AnimatorSet(); - animator.playTogether(fadeAnimator, slideAnimator); - if (startDelay > 0) { - animator.setStartDelay(startDelay); - } - return animator; - } - - /** - * Called to have the inherited class run its own page change animation - * - * @param previousPage The previous page. - */ - abstract protected void onStartPageChangeAnimation(int previousPage); -} diff --git a/common/src/com/android/tv/common/ui/setup/leanback/PagingIndicator.java b/common/src/com/android/tv/common/ui/setup/leanback/PagingIndicator.java deleted file mode 100644 index e2c9be72..00000000 --- a/common/src/com/android/tv/common/ui/setup/leanback/PagingIndicator.java +++ /dev/null @@ -1,377 +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.common.ui.setup.leanback; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorSet; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.os.Build; -import android.support.annotation.ColorInt; -import android.support.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.view.View; - -import com.android.tv.common.R; -import com.android.tv.common.annotation.UsedByReflection; - -import java.util.ArrayList; -import java.util.List; - -/** - * A page indicator with dots. - * @hide - */ -public class PagingIndicator extends View { - // attribute - private final int mDotDiameter; - private final int mDotRadius; - private final int mDotGap; - private final int mArrowDiameter; - private final int mArrowRadius; - private final int mArrowGap; - private final int mShadowRadius; - private Dot[] mDots; - // X position when the dot is selected. - private int[] mDotSelectedX; - // X position when the dot is located to the left of the selected dot. - private int[] mDotSelectedLeftX; - // X position when the dot is located to the right of the selected dot. - private int[] mDotSelectedRightX; - private int mDotCenterY; - - // state - private int mPageCount; - private int mCurrentPage; - private int mPreviousPage; - - // drawing - @ColorInt - private final int mDotFgSelectColor; - private final Paint mBgPaint; - private final Paint mFgPaint; - private final Animator mShowAnimator; - private final Animator mHideAnimator; - private final AnimatorSet mAnimator = new AnimatorSet(); - private final Bitmap mArrow; - private final Rect mArrowRect; - private final float mArrowToBgRatio; - - public PagingIndicator(Context context) { - this(context, null, 0); - } - - public PagingIndicator(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagingIndicator(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - Resources res = getResources(); - mDotRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_radius); - mDotDiameter = mDotRadius * 2; - mDotGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_gap); - mArrowGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_gap); - mArrowDiameter = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_diameter); - mArrowRadius = mArrowDiameter / 2; - mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mDotFgSelectColor = res.getColor(R.color.lb_page_indicator_arrow_background); - int bgColor = res.getColor(R.color.lb_page_indicator_dot); - int shadowColor = res.getColor(R.color.lb_page_indicator_arrow_shadow); - mBgPaint.setColor(bgColor); - mShadowRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_radius); - mFgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - int shadowOffset = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_offset); - mFgPaint.setShadowLayer(mShadowRadius, shadowOffset, shadowOffset, shadowColor); - mArrow = BitmapFactory.decodeResource(res, R.drawable.lb_ic_nav_arrow); - mArrowRect = new Rect(0, 0, mArrow.getWidth(), mArrow.getHeight()); - mArrowToBgRatio = (float) mArrow.getWidth() / (float) mArrowDiameter; - // Initialize animations. - List<Animator> animators = new ArrayList<>(); - mShowAnimator = AnimatorInflater.loadAnimator(getContext(), - R.animator.lb_page_indicator_dot_show); - mHideAnimator = AnimatorInflater.loadAnimator(getContext(), - R.animator.lb_page_indicator_dot_hide); - animators.add(mShowAnimator); - animators.add(mHideAnimator); - mAnimator.playTogether(animators); - // Use software layer to show shadows. - setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - - /** - * Sets the page count. - */ - public void setPageCount(int pages) { - if (pages <= 0) { - throw new IllegalArgumentException("The page count should be a positive integer"); - } - mPageCount = pages; - mDots = new Dot[mPageCount]; - for (int i = 0; i < mPageCount; ++i) { - mDots[i] = new Dot(); - } - calculateDotPositions(); - setSelectedPage(0); - } - - /** - * Called when the page has been selected. - */ - public void onPageSelected(int pageIndex, boolean withAnimation) { - if (mCurrentPage == pageIndex) { - return; - } - if (mAnimator.isStarted()) { - mAnimator.end(); - } - mPreviousPage = mCurrentPage; - if (withAnimation) { - mHideAnimator.setTarget(mDots[mPreviousPage]); - mShowAnimator.setTarget(mDots[pageIndex]); - mAnimator.start(); - } - setSelectedPage(pageIndex); - } - - private void calculateDotPositions() { - int left = getPaddingLeft(); - int top = getPaddingTop(); - int right = getWidth() - getPaddingRight(); - int requiredWidth = getRequiredWidth(); - int mid = (left + right) / 2; - int startLeft = mid - requiredWidth / 2; - mDotSelectedX = new int[mPageCount]; - mDotSelectedLeftX = new int[mPageCount]; - mDotSelectedRightX = new int[mPageCount]; - // mDotSelectedX[0] should be mDotSelectedLeftX[-1] + mArrowGap - mDotSelectedX[0] = startLeft + mDotRadius - mDotGap + mArrowGap; - mDotSelectedLeftX[0] = startLeft + mDotRadius; - mDotSelectedRightX[0] = 0; - for (int i = 1; i < mPageCount; i++) { - mDotSelectedX[i] = mDotSelectedLeftX[i - 1] + mArrowGap; - mDotSelectedLeftX[i] = mDotSelectedLeftX[i - 1] + mDotGap; - mDotSelectedRightX[i] = mDotSelectedX[i - 1] + mArrowGap; - } - mDotCenterY = top + mArrowRadius; - adjustDotPosition(); - } - - @VisibleForTesting - int getPageCount() { - return mPageCount; - } - - @VisibleForTesting - int[] getDotSelectedX() { - return mDotSelectedX; - } - - @VisibleForTesting - int[] getDotSelectedLeftX() { - return mDotSelectedLeftX; - } - - @VisibleForTesting - int[] getDotSelectedRightX() { - return mDotSelectedRightX; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int desiredHeight = getDesiredHeight(); - int height; - switch (MeasureSpec.getMode(heightMeasureSpec)) { - case MeasureSpec.EXACTLY: - height = MeasureSpec.getSize(heightMeasureSpec); - break; - case MeasureSpec.AT_MOST: - height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec)); - break; - case MeasureSpec.UNSPECIFIED: - default: - height = desiredHeight; - break; - } - int desiredWidth = getDesiredWidth(); - int width; - switch (MeasureSpec.getMode(widthMeasureSpec)) { - case MeasureSpec.EXACTLY: - width = MeasureSpec.getSize(widthMeasureSpec); - break; - case MeasureSpec.AT_MOST: - width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec)); - break; - case MeasureSpec.UNSPECIFIED: - default: - width = desiredWidth; - break; - } - setMeasuredDimension(width, height); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - setMeasuredDimension(width, height); - calculateDotPositions(); - } - - private int getDesiredHeight() { - return getPaddingTop() + mArrowDiameter + getPaddingBottom() + mShadowRadius; - } - - private int getRequiredWidth() { - return 2 * mDotRadius + 2 * mArrowGap + (mPageCount - 3) * mDotGap; - } - - private int getDesiredWidth() { - return getPaddingLeft() + getRequiredWidth() + getPaddingRight(); - } - - @Override - protected void onDraw(Canvas canvas) { - for (int i = 0; i < mPageCount; ++i) { - mDots[i].draw(canvas); - } - } - - private void setSelectedPage(int now) { - if (now == mCurrentPage) { - return; - } - - mCurrentPage = now; - adjustDotPosition(); - } - - private void adjustDotPosition() { - for (int i = 0; i < mCurrentPage; ++i) { - mDots[i].deselect(); - mDots[i].mDirection = i == mPreviousPage ? Dot.LEFT : Dot.RIGHT; - mDots[i].mCenterX = mDotSelectedLeftX[i]; - } - mDots[mCurrentPage].select(); - mDots[mCurrentPage].mDirection = mPreviousPage < mCurrentPage ? Dot.LEFT : Dot.RIGHT; - mDots[mCurrentPage].mCenterX = mDotSelectedX[mCurrentPage]; - for (int i = mCurrentPage + 1; i < mPageCount; ++i) { - mDots[i].deselect(); - mDots[i].mDirection = Dot.RIGHT; - mDots[i].mCenterX = mDotSelectedRightX[i]; - } - } - - public class Dot { - static final float LEFT = -1; - static final float RIGHT = 1; - - float mAlpha; - @ColorInt - int mBgColor; - @ColorInt - int mFgColor; - float mTranslationX; - float mCenterX; - float mDiameter; - float mRadius; - float mArrowImageRadius; - float mDirection = RIGHT; - - void select() { - mTranslationX = 0.0f; - mCenterX = 0.0f; - mDiameter = mArrowDiameter; - mRadius = mArrowRadius; - mArrowImageRadius = mRadius * mArrowToBgRatio; - mAlpha = 1.0f; - adjustAlpha(); - } - - void deselect() { - mTranslationX = 0.0f; - mCenterX = 0.0f; - mDiameter = mDotDiameter; - mRadius = mDotRadius; - mArrowImageRadius = mRadius * mArrowToBgRatio; - mAlpha = 0.0f; - adjustAlpha(); - } - - public void adjustAlpha() { - int alpha = Math.round(0xFF * mAlpha); - int red = Color.red(mDotFgSelectColor); - int green = Color.green(mDotFgSelectColor); - int blue = Color.blue(mDotFgSelectColor); - mFgColor = Color.argb(alpha, red, green, blue); - } - - @UsedByReflection - public float getAlpha() { - return mAlpha; - } - - @UsedByReflection - public void setAlpha(float alpha) { - this.mAlpha = alpha; - adjustAlpha(); - invalidate(); - } - - @UsedByReflection - public float getTranslationX() { - return mTranslationX; - } - - @UsedByReflection - public void setTranslationX(float translationX) { - this.mTranslationX = translationX * mDirection; - invalidate(); - } - - @UsedByReflection - public float getDiameter() { - return mDiameter; - } - - @UsedByReflection - public void setDiameter(float diameter) { - this.mDiameter = diameter; - this.mRadius = diameter / 2; - this.mArrowImageRadius = diameter / 2 * mArrowToBgRatio; - invalidate(); - } - - void draw(Canvas canvas) { - float centerX = mCenterX + mTranslationX; - canvas.drawCircle(centerX, mDotCenterY, mRadius, mBgPaint); - if (mAlpha > 0) { - mFgPaint.setColor(mFgColor); - canvas.drawCircle(centerX, mDotCenterY, mRadius, mFgPaint); - canvas.drawBitmap(mArrow, mArrowRect, new Rect((int) (centerX - mArrowImageRadius), - (int) (mDotCenterY - mArrowImageRadius), - (int) (centerX + mArrowImageRadius), - (int) (mDotCenterY + mArrowImageRadius)), null); - } - } - } -} |