diff options
author | Nick Chalko <nchalko@google.com> | 2015-12-09 13:48:17 -0800 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2015-12-11 15:09:19 -0800 |
commit | 1abddd9f6225298066094e20a6c29061b6af4590 (patch) | |
tree | 97d701f8681cca9939c86e5e61523775d4c13aea /common | |
parent | 7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (diff) | |
download | TV-1abddd9f6225298066094e20a6c29061b6af4590.tar.gz |
Sync to ub-tv-heroes at 1.08.301
source change id If9b64d7bbc6e8f77b360e502d34e5452775c0402
Change-Id: I4ffe87911cb85e54880d1d918d1b8fb7bb8cfb7d
Diffstat (limited to 'common')
32 files changed, 2289 insertions, 110 deletions
diff --git a/common/Android.mk b/common/Android.mk index d0c5988b..2f9c32e2 100644 --- a/common/Android.mk +++ b/common/Android.mk @@ -6,7 +6,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_MODULE := tv-common LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := current +LOCAL_SDK_VERSION := system_current LOCAL_RESOURCE_DIR := \ $(TOP)/prebuilts/sdk/current/support/v7/recyclerview/res \ @@ -23,4 +23,5 @@ LOCAL_AAPT_FLAGS := --auto-add-overlay \ --extra-packages android.support.v7.recyclerview \ --extra-packages android.support.v17.leanback \ + include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/common/res/drawable/setup_action_button_done.xml b/common/res/drawable/setup_action_button_done.xml index 66687282..64592b69 100644 --- a/common/res/drawable/setup_action_button_done.xml +++ b/common/res/drawable/setup_action_button_done.xml @@ -19,12 +19,13 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_focused="true"> <shape> - <solid android:color="#01278B"/> + <solid android:color="@color/common_setup_button_done_selected"/> + <corners android:radius="2dp" /> </shape> </item> <item> <shape> - <solid android:color="#01274B"/> + <solid android:color="@android:color/transparent"/> </shape> </item> </selector> diff --git a/common/res/drawable/setup_selector_background.xml b/common/res/drawable/setup_selector_background.xml new file mode 100644 index 00000000..4f3ebfde --- /dev/null +++ b/common/res/drawable/setup_selector_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape> + <solid android:color="#26FFFFFF"/> + <corners android:radius="2dp" /> + </shape> + </item> +</selector> diff --git a/common/res/layout/fragment_setup_multi_pane.xml b/common/res/layout/fragment_setup_multi_pane.xml index 4918a2fb..32bf3188 100644 --- a/common/res/layout/fragment_setup_multi_pane.xml +++ b/common/res/layout/fragment_setup_multi_pane.xml @@ -15,23 +15,48 @@ ~ limitations under the License. --> -<!-- The whole XML will be changed once it's implemented with GuidedStepFragment --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<!-- Need to disable clipping for the shared element transition. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipChildren="false" + android:clipToPadding="false" android:orientation="horizontal"> - <!-- The layout_width should not be 0dp for GuidedStepFragment. see b/24738452 --> + <!-- Guided step fragment container must be at the left of the done button at least by 1 pixel + for the focus navigation. If they overlap, the focus doesn't move from the button to the + fragment container.--> <FrameLayout android:id="@+id/guided_step_fragment_container" - android:layout_width="200dp" + android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_weight="6" - android:background="#01579B" /> - <Button - android:id="@+id/button_done" - android:layout_width="0dp" + android:layout_marginEnd="1dp" + android:clipChildren="false" + android:clipToPadding="false" /> + <FrameLayout + android:id="@+id/done_button_container" + android:layout_width="@dimen/setup_done_button_container_width" android:layout_height="match_parent" - android:layout_weight="1" - android:background="@drawable/setup_action_button_done" - android:text="@string/action_text_done" /> -</LinearLayout> + android:layout_gravity="end" + android:background="@color/common_setup_done_container_background" + android:transitionGroup="false" + android:transitionName="buttonDoneTransition"> + <TextView + android:id="@+id/button_done" + android:layout_width="match_parent" + android:layout_height="45dp" + android:layout_marginStart="24dp" + android:layout_marginEnd="40dp" + android:layout_marginTop="190dp" + android:elevation="0dp" + android:focusable="true" + android:fontFamily="sans-serif-condensed" + android:paddingEnd="12dp" + android:paddingStart="12dp" + android:background="@drawable/setup_action_button_done" + android:gravity="center_vertical|start" + android:text="@string/action_text_done" + android:textColor="#EEEEEE" + android:textSize="14sp" /> + </FrameLayout> +</FrameLayout> diff --git a/common/res/transition/transition_action_background.xml b/common/res/transition/transition_action_background.xml new file mode 100644 index 00000000..02fb9020 --- /dev/null +++ b/common/res/transition/transition_action_background.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"> + <changeTransform + android:duration="666" + android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:reparentWithOverlay="false" /> + <changeBounds + android:duration="666" + android:interpolator="@android:anim/accelerate_decelerate_interpolator" /> +</transitionSet> diff --git a/common/res/values/colors.xml b/common/res/values/colors.xml new file mode 100644 index 00000000..6e44b188 --- /dev/null +++ b/common/res/values/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <!-- Setup screen --> + <color name="common_tv_background">#0288D1</color> + <color name="common_setup_action_background">#0374BF</color> + <color name="common_setup_done_container_background">#04549D</color> + <color name="common_setup_button_done_selected">#26FFFFFF</color> +</resources> diff --git a/common/res/values/dimens.xml b/common/res/values/dimens.xml new file mode 100644 index 00000000..aded2876 --- /dev/null +++ b/common/res/values/dimens.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <!-- Setup screen --> + <dimen name="setup_fragment_enter_from">300dp</dimen> + <dimen name="setup_fragment_enter_to">-300dp</dimen> + + <!-- Guided step fragment --> + <dimen name="setup_guidedstep_guidance_section_width_2pane">600dp</dimen> + <dimen name="setup_guidedstep_guidance_section_width_3pane">476dp</dimen> + <dimen name="setup_done_button_container_width">138dp</dimen> + <dimen name="setup_guidedactions_selector_margin_start">24dp</dimen> + <dimen name="setup_guidedactions_selector_margin_end">24dp</dimen> + <dimen name="setup_guidedactions_selector_margin_top">190dp</dimen> + <dimen name="setup_guidedactions_item_container_padding_start">40dp</dimen> + <dimen name="setup_guidedactions_item_container_padding_end">40dp</dimen> + <dimen name="setup_guidedactions_vertical_padding">12dp</dimen> + <dimen name="setup_guidedactions_vertical_spacing">5dp</dimen> + + <!-- Transition --> + <dimen name="setup_fragment_transition_distance">80dp</dimen> +</resources> diff --git a/common/res/values/integers.xml b/common/res/values/integers.xml index 0ec544dc..c4eb7819 100644 --- a/common/res/values/integers.xml +++ b/common/res/values/integers.xml @@ -17,5 +17,5 @@ <resources> <!-- Setup screen --> - <integer name="setup_slide_anim_duration">250</integer> + <integer name="setup_fragment_transition_duration">333</integer> </resources> diff --git a/common/res/values/styles.xml b/common/res/values/styles.xml index fa551906..7d9575f0 100644 --- a/common/res/values/styles.xml +++ b/common/res/values/styles.xml @@ -15,8 +15,86 @@ ~ limitations under the License. --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="Widget.Setup.GuidanceContainerStyle" parent="Widget.Leanback.GuidanceContainerStyle"> + <item name="android:paddingStart">56dp</item> + <item name="android:paddingEnd">32dp</item> + </style> + + <style name="Widget.Setup.GuidanceTitleStyle" parent="Widget.Leanback.GuidanceTitleStyle"> + <item name="android:layout_alignParentStart">true</item> + <item name="android:layout_centerVertical">false</item> + <item name="android:layout_marginTop">181dp</item> + <item name="android:fontFamily">sans-serif-light</item> + <item name="android:gravity">start</item> + <item name="android:lineSpacingExtra">0sp</item> + <item name="android:lineSpacingMultiplier">1.13778</item> + <item name="android:textColor">#EEEEEE</item> + <item name="android:textSize">34sp</item> + </style> + + <style name="Widget.Setup.GuidanceDescriptionStyle" parent="Widget.Leanback.GuidanceDescriptionStyle"> + <item name="android:layout_marginTop">4dp</item> + <item name="android:fontFamily">sans-serif-light</item> + <item name="android:gravity">start</item> + <item name="android:lineSpacingExtra">0sp</item> + <item name="android:lineSpacingMultiplier">1.465</item> + <item name="android:maxLines">10</item> + <item name="android:textColor">#CCEEEEEE</item> + </style> + + <style name="Widget.Setup.GuidanceBreadcrumbStyle" parent="Widget.Leanback.GuidanceBreadcrumbStyle"> + <item name="android:layout_marginBottom">4dp</item> + <item name="android:fontFamily">sans-serif-condensed</item> + <item name="android:textColor">#B3EEEEEE</item> + <item name="android:textSize">16sp</item> + </style> + <style name="Widget.Setup.GuidedActionsContainerStyle" parent="Widget.Leanback.GuidedActionsContainerStyle"> - <item name="android:background">@android:color/transparent</item> + <item name="android:layout_width">match_parent</item> + <item name="android:background">@color/common_setup_action_background</item> + <item name="android:elevation">0dp</item> + <item name="android:transitionName">guidedActionsBackgroundTransition</item> + </style> + + <style name="Widget.Setup.GuidedActionsSelectorStyle" parent="Widget.Leanback.GuidedActionsSelectorStyle"> + <item name="android:layout_centerVertical">false</item> + <item name="android:layout_marginStart">@dimen/setup_guidedactions_selector_margin_start</item> + <item name="android:layout_marginEnd">@dimen/setup_guidedactions_selector_margin_end</item> + <item name="android:layout_marginTop">@dimen/setup_guidedactions_selector_margin_top</item> + <item name="android:background">@drawable/setup_selector_background</item> + <item name="android:elevation">0dp</item> + </style> + + <style name="Widget.Setup.GuidedActionsListStyle" parent="Widget.Leanback.GuidedActionsListStyle"> + <item name="android:elevation">0dp</item> + </style> + + <style name="Widget.Setup.GuidedActionItemContainerStyle" parent="Widget.Leanback.GuidedActionItemContainerStyle"> + <item name="android:layout_height">64dp</item> + <item name="android:paddingBottom">@dimen/setup_guidedactions_vertical_padding</item> + <item name="android:paddingEnd">@dimen/setup_guidedactions_item_container_padding_end</item> + <item name="android:paddingStart">@dimen/setup_guidedactions_item_container_padding_start</item> + <item name="android:paddingTop">@dimen/setup_guidedactions_vertical_padding</item> + <item name="android:transitionGroup">true</item> + </style> + + <style name="Widget.Setup.GuidedActionItemCheckmarkStyle" parent="Widget.Leanback.GuidedActionItemCheckmarkStyle"> + <item name="android:visibility">gone</item> + </style> + + <style name="Widget.Setup.GuidedActionItemTitleStyle" parent="Widget.Leanback.GuidedActionItemTitleStyle"> + <item name="android:layout_height">18dp</item> + <item name="android:fontFamily">sans-serif</item> + <item name="android:textColor">#EEEEEE</item> + <item name="android:textSize">14sp</item> + </style> + + <style name="Widget.Setup.GuidedActionItemDescriptionStyle" parent="Widget.Leanback.GuidedActionItemDescriptionStyle"> + <item name="android:layout_height">17dp</item> + <item name="android:alpha">0.5</item> + <item name="android:fontFamily">sans-serif</item> + <item name="android:textColor">#EEEEEE</item> + <item name="android:textSize">12sp</item> </style> </resources> diff --git a/common/res/values/themes.xml b/common/res/values/themes.xml index ae76cf90..6d7ac0a5 100644 --- a/common/res/values/themes.xml +++ b/common/res/values/themes.xml @@ -17,6 +17,18 @@ <resources> <style name="Theme.Setup.GuidedStep" parent="Theme.Leanback.GuidedStep"> + <item name="android:windowBackground">@color/common_tv_background</item> + <item name="android:windowEnterTransition">@null</item> + <item name="guidanceContainerStyle">@style/Widget.Setup.GuidanceContainerStyle</item> + <item name="guidanceTitleStyle">@style/Widget.Setup.GuidanceTitleStyle</item> + <item name="guidanceDescriptionStyle">@style/Widget.Setup.GuidanceDescriptionStyle</item> + <item name="guidanceBreadcrumbStyle">@style/Widget.Setup.GuidanceBreadcrumbStyle</item> <item name="guidedActionsContainerStyle">@style/Widget.Setup.GuidedActionsContainerStyle</item> + <item name="guidedActionsSelectorStyle">@style/Widget.Setup.GuidedActionsSelectorStyle</item> + <item name="guidedActionsListStyle">@style/Widget.Setup.GuidedActionsListStyle</item> + <item name="guidedActionItemContainerStyle">@style/Widget.Setup.GuidedActionItemContainerStyle</item> + <item name="guidedActionItemCheckmarkStyle">@style/Widget.Setup.GuidedActionItemCheckmarkStyle</item> + <item name="guidedActionItemTitleStyle">@style/Widget.Setup.GuidedActionItemTitleStyle</item> + <item name="guidedActionItemDescriptionStyle">@style/Widget.Setup.GuidedActionItemDescriptionStyle</item> </style> </resources> diff --git a/common/src/com/android/tv/common/BooleanSystemProperty.java b/common/src/com/android/tv/common/BooleanSystemProperty.java index 77a6023c..21e575a1 100644 --- a/common/src/com/android/tv/common/BooleanSystemProperty.java +++ b/common/src/com/android/tv/common/BooleanSystemProperty.java @@ -98,4 +98,9 @@ public class BooleanSystemProperty { } return mValue; } + + @Override + public String toString() { + return "SystemProperty[" + mKey + "]=" + getValue(); + } } diff --git a/common/src/com/android/tv/common/TvCommonConstants.java b/common/src/com/android/tv/common/TvCommonConstants.java index e01eb52f..a3c20d94 100644 --- a/common/src/com/android/tv/common/TvCommonConstants.java +++ b/common/src/com/android/tv/common/TvCommonConstants.java @@ -16,6 +16,7 @@ package com.android.tv.common; +import android.media.tv.TvInputInfo; import android.os.Build; /** @@ -39,17 +40,27 @@ public final class TvCommonConstants { * activity successfully finishes. */ public static final String INTENT_ACTION_INPUT_SETUP = - "com.android.tv.intent.action.INPUT_SETUP"; + "com.android.tv.action.LAUNCH_INPUT_SETUP"; + /** - * A constant for the key to indicate a TV input ID for the intent action + * A constant of the key to indicate a TV input ID for the intent action * {@link INTENT_ACTION_INPUT_SETUP}. * * <p>Value type: String */ - public static final String EXTRA_INPUT_ID = - "com.android.tv.intent.extra.INPUT_ID"; + public static final String EXTRA_INPUT_ID = TvInputInfo.EXTRA_INPUT_ID; + + /** + * A constant of the key for intent to launch actual TV input setup activity used with + * {@link INTENT_ACTION_INPUT_SETUP}. + * + * <p>Value type: Intent (Parcelable) + */ + public static final String EXTRA_SETUP_INTENT = + "com.android.tv.extra.SETUP_INTENT"; + /** - * A constant for the key to indicate an Activity launch intent for the intent action + * A constant of the key to indicate an Activity launch intent for the intent action * {@link INTENT_ACTION_INPUT_SETUP}. * * <p>Value type: Intent (Parcelable) diff --git a/common/src/com/android/tv/common/TvCommonUtils.java b/common/src/com/android/tv/common/TvCommonUtils.java new file mode 100644 index 00000000..a88dd3a8 --- /dev/null +++ b/common/src/com/android/tv/common/TvCommonUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright 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.Intent; +import android.media.tv.TvInputInfo; + +/** + * Util class for common use in TV app and inputs. + */ +public final class TvCommonUtils { + private TvCommonUtils() { } + + /** + * Returns an intent to start the setup activity for the TV input using {@link + * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}. + */ + public static Intent createSetupIntent(Intent originalSetupIntent, String inputId) { + if (originalSetupIntent == null) { + return null; + } + Intent setupIntent = new Intent(originalSetupIntent); + if (!TvCommonConstants.INTENT_ACTION_INPUT_SETUP.equals(originalSetupIntent.getAction())) { + Intent intentContainer = new Intent(TvCommonConstants.INTENT_ACTION_INPUT_SETUP); + intentContainer.putExtra(TvCommonConstants.EXTRA_SETUP_INTENT, originalSetupIntent); + intentContainer.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId); + setupIntent = intentContainer; + } + return setupIntent; + } + + /** + * Returns an intent to start the setup activity for this TV input using {@link + * TvCommonConstants#INTENT_ACTION_INPUT_SETUP}. + */ + public static Intent createSetupIntent(TvInputInfo input) { + return createSetupIntent(input.createSetupIntent(), input.getId()); + } + + /** + * Checks if this application is running in tests. + * + * <p>{@link android.app.ActivityManager#isRunningInTestHarness} doesn't return {@code true} for + * the usual devices even the application is running in tests. We need to figure it out by + * checking whether the class in tv-tests-common module can be loaded or not. + */ + public static boolean isRunningInTest() { + try { + Class.forName("com.android.tv.testing.Utils"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/common/src/com/android/tv/common/dvr/DvrSessionClient.java b/common/src/com/android/tv/common/dvr/DvrSessionClient.java new file mode 100644 index 00000000..b7dde7ff --- /dev/null +++ b/common/src/com/android/tv/common/dvr/DvrSessionClient.java @@ -0,0 +1,149 @@ +/* + * 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.dvr; + +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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A session used for recording. + */ +public class DvrSessionClient { + @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 {} + 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 = 10; + + private boolean mRecordStarted; + private Callback mCallback; + private TvView mTvView; + + public DvrSessionClient(Context context) { + mTvView = new TvView(context); + } + + /** + * Connects the session to a specific input {@code inputId}. + */ + public void connect(String inputId, Callback callback) { + mCallback = callback; + Bundle bundle = new Bundle(); + bundle.putBoolean(DvrUtils.BUNDLE_IS_DVR, true); + mTvView.tune(inputId, TvContract.buildChannelUri(0), bundle); + mTvView.sendAppPrivateCommand(DvrUtils.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(DvrUtils.BUNDLE_MEDIA_URI, null); + Uri mediaUri = mediaUriString == null ? null : Uri.parse(mediaUriString); + if (DvrUtils.EVENT_TYPE_CONNECTED.equals(eventType)) { + mCallback.onConnected(); + } else if (DvrUtils.EVENT_TYPE_RECORD_STARTED.equals(eventType)) { + mCallback.onRecordStarted(mediaUri); + } else if (DvrUtils.EVENT_TYPE_RECORD_STOPPED.equals(eventType)) { + int reason = eventArgs.getInt(DvrUtils.BUNDLE_STOPPED_REASON); + mCallback.onRecordStopped(mediaUri, reason); + } else if (DvrUtils.EVENT_TYPE_DELETED.equals(eventType)) { + mCallback.onRecordDeleted(mediaUri); + } + } + + // TODO: handle track select. + }); + } + + /** + * Releases the session. + */ + public void release() { + mTvView.reset(); + mCallback = null; + } + + /** + * Starts recording. + */ + public void startRecord(Uri channelUri, Uri mediaUri) { + if (mRecordStarted) { + throw new IllegalStateException("Don't reuse the session for simple implementation"); + } + mRecordStarted = true; + Bundle params = DvrUtils.buildMediaUri(mediaUri); + params.putString(DvrUtils.BUNDLE_CHANNEL_URI, channelUri.toString()); + mTvView.sendAppPrivateCommand(DvrUtils.APP_PRIV_START_RECORD, params); + } + + /** + * Stops recording. + */ + public void stopRecord() { + if (!mRecordStarted) { + return; + } + mRecordStarted = false; + mTvView.sendAppPrivateCommand(DvrUtils.APP_PRIV_STOP_RECORD, null); + } + + /** + * Deletes a recorded media. + */ + public void delete(Uri mediaUri) { + mTvView.sendAppPrivateCommand(DvrUtils.APP_PRIV_DELETE, DvrUtils.buildMediaUri(mediaUri)); + } + + public abstract static class Callback { + 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) { } + } +} diff --git a/common/src/com/android/tv/common/dvr/DvrTvInputService.java b/common/src/com/android/tv/common/dvr/DvrTvInputService.java new file mode 100644 index 00000000..ecf90656 --- /dev/null +++ b/common/src/com/android/tv/common/dvr/DvrTvInputService.java @@ -0,0 +1,447 @@ +/* + * 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.dvr; + +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.Bundle; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Surface; + +import java.util.List; + +/** + * {@link TvInputService} class supporting DVR feature. + */ +public abstract class DvrTvInputService extends TvInputService { + private static final String TAG = "DvrTvInputService"; + private static final boolean DEBUG = false; + // TODO: use Features.DVR. + private static final boolean FEATURE_DVR = false; + + @Override + public final Session onCreateSession(String inputId) { + if (FEATURE_DVR) { + return new InternalSession(this, inputId); + } else { + return onCreatePlaybackSession(inputId); + } + } + + /** + * Called when {@link com.android.tv.common.dvr.DvrSession#connect} is called. + */ + protected DvrSession 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(DvrUtils.APP_PRIV_CREATE_DVR_SESSION)) { + if (mSessionImpl == null) { + mSessionImpl = onCreateDvrSession(mInputId); + if (mSessionImpl != null) { + mSessionImpl.setPassthroughSession(this); + notifySessionEvent(DvrUtils.EVENT_TYPE_CONNECTED, null); + } + } + } else if (action.equals(DvrUtils.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 DvrSession}. 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 com.android.tv.common.dvr.DvrSession} to record contents. + */ + public static abstract class DvrSession extends BaseSession { + public DvrSession(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 + } + + /** + * Called when it starts to record {@code channelUri}. {@link #notifyRecordStarted()} + * should be called as soon as starting recording. + */ + public abstract void onStartRecord(Uri channelUri, Uri mediaUri); + + /** + * Called when it stops to record. + */ + public abstract void onStopRecord(); + + /** + * Called when it is requested to delete {@code mediaUri}. + */ + public abstract void onDelete(Uri mediaUri); + + /** + * Notifies when recording starts. It is an response of {@link #onStartRecord}. + */ + public void notifyRecordStarted(Uri mediaUri) { + notifySessionEvent(DvrUtils.EVENT_TYPE_RECORD_STARTED, + DvrUtils.buildMediaUri(mediaUri)); + } + + /** + * Notifies when recording is unexpectedly stopped. + */ + public void notifyRecordUnexpectedlyStopped(Uri mediaUri, int reason) { + Bundle params = DvrUtils.buildMediaUri(mediaUri); + params.putInt(DvrUtils.BUNDLE_STOPPED_REASON, reason); + notifySessionEvent(DvrUtils.EVENT_TYPE_RECORD_STOPPED, params); + } + + /** + * Notifies when the recording {@code mediaUri} is deleted. + */ + public void notifyDeleted(Uri mediaUri) { + notifySessionEvent(DvrUtils.EVENT_TYPE_DELETED, DvrUtils.buildMediaUri(mediaUri)); + } + + /** + * Notifies when the deletion of the recording {@code mediaUri} is requested through + * {@link #onDelete} but failed. + */ + public void notifyDeleteFailed(Uri mediaUri, int reason) { + Bundle params = DvrUtils.buildMediaUri(mediaUri); + params.putInt(DvrUtils.BUNDLE_DELETE_FAILED_REASON, reason); + notifySessionEvent(DvrUtils.EVENT_TYPE_DELETE_FAILED, params); + } + + @Override + public void onAppPrivateCommand(String action, Bundle data) { + if (DvrUtils.APP_PRIV_START_RECORD.equals(action)) { + onStartRecord(Uri.parse(data.getString(DvrUtils.BUNDLE_CHANNEL_URI)), + Uri.parse(data.getString(DvrUtils.BUNDLE_CHANNEL_URI))); + } else if (DvrUtils.APP_PRIV_STOP_RECORD.equals(action)) { + onStopRecord(); + } else if (DvrUtils.APP_PRIV_DELETE.equals(action)) { + onDelete(Uri.parse(data.getString(DvrUtils.BUNDLE_CHANNEL_URI))); + } + } + } + + /** + * Session linked to {@link android.media.tv.TvView} to tune to a channel or play an recording. + */ + public static abstract class PlaybackSession extends BaseSession { + public PlaybackSession(Context context) { + super(context); + } + + /** + * 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) { } + + @Override + public final boolean onTune(Uri channelUri, Bundle params) { + if (params != null && params.getBoolean(DvrUtils.BUNDLE_IS_DVR, false)) { + notifySessionEvent(DvrUtils.EVENT_TYPE_CONNECTED, null); + return true; + } else if (params != null && params.containsKey(DvrUtils.BUNDLE_MEDIA_URI)) { + onPlayMedia(Uri.parse(params.getString(DvrUtils.BUNDLE_CHANNEL_URI))); + return true; + } else { + return onTune(channelUri); + } + } + } +} diff --git a/common/src/com/android/tv/common/dvr/DvrTvView.java b/common/src/com/android/tv/common/dvr/DvrTvView.java new file mode 100644 index 00000000..247db191 --- /dev/null +++ b/common/src/com/android/tv/common/dvr/DvrTvView.java @@ -0,0 +1,67 @@ +/* + * 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.dvr; + +import android.content.Context; +import android.media.tv.TvContract; +import android.media.tv.TvView; +import android.net.Uri; +import android.os.Bundle; +import android.util.AttributeSet; + +/** + * Extend {@link TvView} to support recording playback. + */ +public class DvrTvView extends TvView { + + public DvrTvView(Context context) { + this(context, null, 0); + } + + public DvrTvView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DvrTvView(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), DvrUtils.buildMediaUri(mediaUri)); + } + + @Override + public void tune(String inputId, Uri channelUri, Bundle params) { + super.tune(inputId, channelUri, params); + sendAppPrivateCommand(DvrUtils.APP_PRIV_CREATE_PLAYBACK_SESSION, null); + } + + public void setTimeShiftPositionCallback(TimeShiftPositionCallback2 callback) { + // TODO: implement + } + + /** + * 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/dvr/DvrUtils.java b/common/src/com/android/tv/common/dvr/DvrUtils.java new file mode 100644 index 00000000..0a3e536b --- /dev/null +++ b/common/src/com/android/tv/common/dvr/DvrUtils.java @@ -0,0 +1,53 @@ +/* + * 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.dvr; + +import android.net.Uri; +import android.os.Bundle; + +public class DvrUtils { + 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_RECORD_STARTED = "event_type_record_started"; + static final String EVENT_TYPE_RECORD_STOPPED = "event_type_record_stopped"; + static final String EVENT_TYPE_DELETED = "event_type_deleted"; + static final String EVENT_TYPE_DELETE_FAILED = "event_type_delete_failed"; + + 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"; + static final String APP_PRIV_START_RECORD = "app_priv_start_record"; + static final String APP_PRIV_STOP_RECORD = "app_priv_stop_record"; + static final String APP_PRIV_DELETE = "app_priv_delete"; + // 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: int + static final String BUNDLE_STOPPED_REASON = "stopped_reason"; + // Type: int + static final String BUNDLE_DELETE_FAILED_REASON = "delete_failed_reason"; + + static Bundle buildMediaUri(Uri mediaUri) { + Bundle params = new Bundle(); + params.putString(DvrUtils.BUNDLE_MEDIA_URI, mediaUri.toString()); + return params; + } +} diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java index 2a676948..f60b2048 100644 --- a/common/src/com/android/tv/common/feature/FeatureUtils.java +++ b/common/src/com/android/tv/common/feature/FeatureUtils.java @@ -18,6 +18,8 @@ package com.android.tv.common.feature; import android.content.Context; +import java.util.Arrays; + /** * Static utilities for features. */ @@ -39,7 +41,13 @@ public class FeatureUtils { } return false; } + + @Override + public String toString() { + return "or(" + Arrays.asList(features) + ")"; + } }; + } /** @@ -58,6 +66,11 @@ public class FeatureUtils { } return true; } + + @Override + public String toString() { + return "and(" + Arrays.asList(features) + ")"; + } }; } @@ -69,6 +82,11 @@ public class FeatureUtils { public boolean isEnabled(Context context) { return true; } + + @Override + public String toString() { + return "on"; + } }; /** @@ -79,6 +97,11 @@ public class FeatureUtils { public boolean isEnabled(Context context) { return false; } + + @Override + public String toString() { + return "off"; + } }; private FeatureUtils() { diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/GServiceFeature.java index 10b369f1..9e6e11a6 100644 --- a/common/src/com/android/tv/common/feature/GServiceFeature.java +++ b/common/src/com/android/tv/common/feature/GServiceFeature.java @@ -38,4 +38,9 @@ public class GServiceFeature implements Feature { // GServices is not available outside of Google. return mDefaultValue; } + + @Override + public String toString() { + return "GService[hash=" + mKey.hashCode() + "]"; + } } diff --git a/common/src/com/android/tv/common/feature/PropertyFeature.java b/common/src/com/android/tv/common/feature/PropertyFeature.java index 27bccda1..fdcffa04 100644 --- a/common/src/com/android/tv/common/feature/PropertyFeature.java +++ b/common/src/com/android/tv/common/feature/PropertyFeature.java @@ -49,4 +49,9 @@ public final class PropertyFeature implements Feature { public boolean isEnabled(Context context) { return mProperty.getValue(); } + + @Override + public String toString() { + return mProperty.toString(); + } } diff --git a/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java new file mode 100644 index 00000000..eb5c805f --- /dev/null +++ b/common/src/com/android/tv/common/feature/SharedPreferencesFeature.java @@ -0,0 +1,82 @@ +/* + * 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.feature; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * Feature controlled by shared preferences. + */ +public final class SharedPreferencesFeature implements Feature { + private static final String TAG = "SharedPreferencesFeature"; + private static final boolean DEBUG = false; + + private static final String SHARED_PREFERENCE = "sharePreferencesFeatures"; + private String mKey; + private boolean mEnabled; + private boolean mDefaultValue; + private SharedPreferences mSharedPreferences; + private final Feature mBaseFeature; + + /** + * Create SharedPreferences controlled feature. + * + * @param key the SharedPreferences key. + * @param defaultValue the value to return if the property is undefined or empty. + * @param baseFeature if {@code baseFeature} is turned off, this feature is always disabled. + */ + public SharedPreferencesFeature(String key, boolean defaultValue, Feature baseFeature) { + mKey = key; + mDefaultValue = defaultValue; + mBaseFeature = baseFeature; + } + + @Override + public boolean isEnabled(Context context) { + if (!mBaseFeature.isEnabled(context)) { + return false; + } + if (mSharedPreferences == null) { + mSharedPreferences = context.getSharedPreferences( + SHARED_PREFERENCE, Context.MODE_PRIVATE); + mEnabled = mSharedPreferences.getBoolean(mKey, mDefaultValue); + } + if (DEBUG) Log.d(TAG, mKey + " is " + mEnabled); + return mEnabled; + } + + @Override + public String toString() { + return "SharedPreferencesFeature:key=" + mKey + ",value=" + mEnabled; + } + + public void setEnabled(Context context, boolean enable) { + if (DEBUG) Log.d(TAG, mKey + " is set to " + enable); + if (mSharedPreferences == null) { + mSharedPreferences = context.getSharedPreferences( + SHARED_PREFERENCE, Context.MODE_PRIVATE); + mEnabled = enable; + mSharedPreferences.edit().putBoolean(mKey, enable).apply(); + } else if (mEnabled != enable) { + mEnabled = enable; + mSharedPreferences.edit().putBoolean(mKey, enable).apply(); + } + + } +} diff --git a/common/src/com/android/tv/common/feature/TestableFeature.java b/common/src/com/android/tv/common/feature/TestableFeature.java new file mode 100644 index 00000000..db546ec9 --- /dev/null +++ b/common/src/com/android/tv/common/feature/TestableFeature.java @@ -0,0 +1,89 @@ +/* + * 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.feature; + +import android.app.ActivityManager; +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.common.TvCommonUtils; + +/** + * When run in a test harness this feature can be turned on or off, overriding the normal value. + * + * <p><b>Warning</b> making a feature testable will cause the code to stay in the APK and + * could leak unreleased features. + */ +public class TestableFeature implements Feature { + private final static String TAG = "TestableFeature"; + private final Feature mDelegate; + private Boolean mTestValue = null; + + public static TestableFeature createTestableFeature(Feature delegate) { + return new TestableFeature(delegate); + } + + private TestableFeature(Feature delegate) { + mDelegate = delegate; + } + + @VisibleForTesting + public void enableForTest() { + if (TvCommonUtils.isRunningInTest()) { + Log.e(TAG, "TestableFeatures should only be changed in tests. Ignoring"); + mTestValue = true; + } + } + + @VisibleForTesting + public void disableForTests() { + if (TvCommonUtils.isRunningInTest()) { + Log.e(TAG, "TestableFeatures should only be changed in tests. Ignoring"); + mTestValue = false; + } + } + + @VisibleForTesting + public void resetForTests() { + if (TvCommonUtils.isRunningInTest()) { + Log.e(TAG, "TestableFeatures should only be changed in tests. Ignoring"); + mTestValue = null; + } + } + + @Override + public boolean isEnabled(Context context) { + if (TvCommonUtils.isRunningInTest() && mTestValue != null) { + return mTestValue; + } + return mDelegate.isEnabled(context); + } + + @Override + public String toString() { + String msg = mDelegate.toString(); + if (TvCommonUtils.isRunningInTest()) { + if (mTestValue == null) { + msg = "Testable Feature is unchanged: " + msg; + } else { + msg = "Testable Feature is " + (mTestValue ? "on" : "off") + " was " + msg; + } + } + return msg; + } +} diff --git a/common/src/com/android/tv/common/ui/setup/SetupFragment.java b/common/src/com/android/tv/common/ui/setup/SetupFragment.java index 46501652..0ae96d63 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupFragment.java @@ -16,32 +16,40 @@ package com.android.tv.common.ui.setup; -import android.animation.Animator; -import android.animation.ObjectAnimator; import android.app.Fragment; -import android.content.Context; -import android.graphics.Point; -import android.hardware.display.DisplayManager; import android.os.Bundle; -import android.support.v4.view.animation.LinearOutSlowInInterpolator; -import android.view.Display; +import android.support.annotation.IntDef; +import android.transition.Transition; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import com.android.tv.common.R; +import com.android.tv.common.ui.setup.animation.FadeAndShortSlide; +import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * A fragment which slides when it is entering/exiting. */ public abstract class SetupFragment extends Fragment { - public static final int ANIM_ENTER = 1; - public static final int ANIM_EXIT = 2; - public static final int ANIM_POP_ENTER = 3; - public static final int ANIM_POP_EXIT = 4; + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = {FRAGMENT_ENTER_TRANSITION, FRAGMENT_EXIT_TRANSITION, + FRAGMENT_REENTER_TRANSITION, FRAGMENT_RETURN_TRANSITION}) + public @interface FragmentTransitionType {} + protected static final int FRAGMENT_ENTER_TRANSITION = 0x01; + protected static final int FRAGMENT_EXIT_TRANSITION = FRAGMENT_ENTER_TRANSITION << 1; + protected static final int FRAGMENT_REENTER_TRANSITION = FRAGMENT_ENTER_TRANSITION << 2; + protected static final int FRAGMENT_RETURN_TRANSITION = FRAGMENT_ENTER_TRANSITION << 3; - private static int sScreenWidth; + public SetupFragment() { + setAllowEnterTransitionOverlap(false); + setAllowReturnTransitionOverlap(false); + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -58,39 +66,6 @@ public abstract class SetupFragment extends Fragment { */ protected abstract int getLayoutResourceId(); - @Override - public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { - if (sScreenWidth == 0) { - DisplayManager displayManager = - (DisplayManager) getActivity().getSystemService(Context.DISPLAY_SERVICE); - Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - Point size = new Point(); - display.getSize(size); - sScreenWidth = size.x; - } - - switch (nextAnim) { - case ANIM_ENTER: - return createTranslateAnimator(sScreenWidth, 0); - case ANIM_EXIT: - return createTranslateAnimator(0, -sScreenWidth); - case ANIM_POP_ENTER: - return createTranslateAnimator(-sScreenWidth, 0); - case ANIM_POP_EXIT: - return createTranslateAnimator(0, sScreenWidth); - } - return super.onCreateAnimator(transit, enter, nextAnim); - } - - private Animator createTranslateAnimator(int start, int end) { - ObjectAnimator animator = new ObjectAnimator(); - animator.setProperty(View.TRANSLATION_X); - animator.setFloatValues(start, end); - animator.setDuration(getResources().getInteger(R.integer.setup_slide_anim_duration)); - animator.setInterpolator(new LinearOutSlowInInterpolator()); - return animator; - } - protected void setOnClickAction(View view, final int actionId) { view.setOnClickListener(new OnClickListener() { @Override @@ -103,4 +78,124 @@ public abstract class SetupFragment extends Fragment { protected void onActionClick(int actionId) { SetupActionHelper.onActionClick(this, actionId); } + + /** + * Enables fragment transition according to the given {@code mask}. + * + * @param mask This value is the combination of {@link #FRAGMENT_ENTER_TRANSITION}, + * {@link #FRAGMENT_EXIT_TRANSITION}, {@link #FRAGMENT_REENTER_TRANSITION}, and + * {@link #FRAGMENT_RETURN_TRANSITION}. + */ + public void enableFragmentTransition(@FragmentTransitionType int mask) { + setEnterTransition((mask & FRAGMENT_ENTER_TRANSITION) == 0 ? null + : createTransition(Gravity.END)); + setExitTransition((mask & FRAGMENT_EXIT_TRANSITION) == 0 ? null + : createTransition(Gravity.START)); + setReenterTransition((mask & FRAGMENT_REENTER_TRANSITION) == 0 ? null + : createTransition(Gravity.START)); + setReturnTransition((mask & FRAGMENT_RETURN_TRANSITION) == 0 ? null + : createTransition(Gravity.END)); + } + + /** + * Sets the transition with the given {@code slidEdge}. + */ + protected void setFragmentTransition(@FragmentTransitionType int transitionType, + int slideEdge) { + switch (transitionType) { + case FRAGMENT_ENTER_TRANSITION: + setEnterTransition(createTransition(slideEdge)); + break; + case FRAGMENT_EXIT_TRANSITION: + setExitTransition(createTransition(slideEdge)); + break; + case FRAGMENT_REENTER_TRANSITION: + setReenterTransition(createTransition(slideEdge)); + break; + case FRAGMENT_RETURN_TRANSITION: + setReturnTransition(createTransition(slideEdge)); + break; + } + } + + private Transition createTransition(int slideEdge) { + return new SetupAnimationHelper.TransitionBuilder() + .setSlideEdge(slideEdge) + .setParentIdsForDelay(getParentIdsForDelay()) + .setExcludeIds(getExcludedTargetIds()) + .build(); + } + + /** + * Sets the distance of the fragment transition. + */ + public void setTransitionDistance(int distance) { + Transition transition = getEnterTransition(); + if (transition instanceof FadeAndShortSlide) { + ((FadeAndShortSlide) transition).setDistance(distance); + } + transition = getExitTransition(); + if (transition instanceof FadeAndShortSlide) { + ((FadeAndShortSlide) transition).setDistance(distance); + } + transition = getReenterTransition(); + if (transition instanceof FadeAndShortSlide) { + ((FadeAndShortSlide) transition).setDistance(distance); + } + transition = getReturnTransition(); + if (transition instanceof FadeAndShortSlide) { + ((FadeAndShortSlide) transition).setDistance(distance); + } + } + + /** + * Sets the duration of the fragment transition. + */ + public void setTransitionDuration(long duration) { + Transition transition = getEnterTransition(); + if (transition != null) { + transition.setDuration(duration); + } + transition = getExitTransition(); + if (transition != null) { + transition.setDuration(duration); + } + transition = getReenterTransition(); + if (transition != null) { + transition.setDuration(duration); + } + transition = getReturnTransition(); + if (transition != null) { + transition.setDuration(duration); + } + } + + /** + * Returns the ID's of the view's whose descendants will perform delayed move. + * + * @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder + * #setParentIdsForDelay + */ + protected int[] getParentIdsForDelay() { + return null; + } + + /** + * Sets the ID's of the views which will not be included in the transition. + * + * @see com.android.tv.common.ui.setup.animation.SetupAnimationHelper.TransitionBuilder + * #setExcludeIds + */ + protected int[] getExcludedTargetIds() { + return null; + } + + /** + * Returns the ID's of the shared elements. + * + * <p>Note that the shared elements should have their own transition names. + */ + public int[] getSharedElementIds() { + return null; + } } diff --git a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java index c744a4ab..65575adc 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java @@ -16,17 +16,84 @@ package com.android.tv.common.ui.setup; +import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; + +import com.android.tv.common.R; /** * A fragment for channel source info/setup. */ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { + /** + * Key of the argument which indicate whether the parent of this fragment has three panes. + * + * <p>Value type: boolean + */ + public static final String KEY_THREE_PANE = "key_three_pane"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + Bundle arguments = getArguments(); + view.findViewById(R.id.action_fragment).setPadding(0, 0, 0, 0); + if (arguments != null && arguments.getBoolean(KEY_THREE_PANE, false)) { + boolean isRtl = view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + // Content fragment. + LayoutParams layoutParams = view.findViewById(R.id.content_fragment).getLayoutParams(); + layoutParams.width = getResources().getDimensionPixelOffset( + R.dimen.setup_guidedstep_guidance_section_width_3pane); + int doneButtonWidth = getResources().getDimensionPixelOffset( + R.dimen.setup_done_button_container_width); + // Guided actions selector. + int endMargin = getResources().getDimensionPixelOffset( + R.dimen.setup_guidedactions_selector_margin_end); + MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.findViewById( + R.id.guidedactions_selector).getLayoutParams(); + if (isRtl) { + marginLayoutParams.leftMargin = endMargin + doneButtonWidth; + } else { + marginLayoutParams.rightMargin = endMargin + doneButtonWidth; + } + // Guided actions list + marginLayoutParams = (MarginLayoutParams) view.findViewById(R.id.guidedactions_list) + .getLayoutParams(); + if (isRtl) { + marginLayoutParams.leftMargin = doneButtonWidth; + } else { + marginLayoutParams.rightMargin = doneButtonWidth; + } + } else { + // Content fragment. + LayoutParams layoutParams = view.findViewById(R.id.content_fragment).getLayoutParams(); + layoutParams.width = getResources().getDimensionPixelOffset( + R.dimen.setup_guidedstep_guidance_section_width_2pane); + } + // gridView Alignment + VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); + int offset = getResources().getDimensionPixelOffset( + R.dimen.setup_guidedactions_selector_margin_top); + gridView.setWindowAlignmentOffset(offset); + gridView.setWindowAlignmentOffsetPercent(0); + gridView.setItemAlignmentOffsetPercent(0); + ((ViewGroup) view.findViewById(R.id.guidedactions_list)).setTransitionGroup(false); + // Needed for the shared element transition. + // content_frame is defined in leanback. + ViewGroup group = (ViewGroup) view.findViewById(R.id.content_frame); + group.setClipChildren(false); + group.setClipToPadding(false); + return view; + } + @Override public GuidanceStylist onCreateGuidanceStylist() { return new GuidanceStylist() { @@ -47,4 +114,9 @@ public abstract class SetupGuidedStepFragment extends GuidedStepFragment { public void onGuidedActionClicked(GuidedAction action) { SetupActionHelper.onActionClick(this, (int) action.getId()); } + + @Override + protected void onProvideFragmentTransitions() { + // Don't use the fragment transition defined in GuidedStepFragment. + } } diff --git a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java index 59416eff..fa946ae3 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java +++ b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java @@ -16,12 +16,11 @@ package com.android.tv.common.ui.setup; -import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; import com.android.tv.common.R; @@ -31,22 +30,32 @@ import com.android.tv.common.R; public abstract class SetupMultiPaneFragment extends SetupFragment { public static final int ACTION_DONE = 1; + public SetupMultiPaneFragment() { + enableFragmentTransition(FRAGMENT_ENTER_TRANSITION | FRAGMENT_EXIT_TRANSITION + | FRAGMENT_REENTER_TRANSITION | FRAGMENT_RETURN_TRANSITION); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - getFragmentManager().beginTransaction() - .replace(R.id.guided_step_fragment_container, getContentFragment()).commit(); - View doneButton = view.findViewById(R.id.button_done); + SetupGuidedStepFragment contentFragment = onCreateContentFragment(); + getChildFragmentManager().beginTransaction() + .replace(R.id.guided_step_fragment_container, contentFragment).commit(); if (needsDoneButton()) { - doneButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View paramView) { - SetupActionHelper.onActionClick(SetupMultiPaneFragment.this, ACTION_DONE); - } - }); + setOnClickAction(view.findViewById(R.id.button_done), ACTION_DONE); } else { - doneButton.setVisibility(View.GONE); + View doneButtonContainer = view.findViewById(R.id.done_button_container); + if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { + ((MarginLayoutParams) doneButtonContainer.getLayoutParams()).rightMargin = + -getResources().getDimensionPixelOffset( + R.dimen.setup_done_button_container_width); + } else { + ((MarginLayoutParams) doneButtonContainer.getLayoutParams()).leftMargin = + -getResources().getDimensionPixelOffset( + R.dimen.setup_done_button_container_width); + } + view.findViewById(R.id.button_done).setFocusable(false); } return view; } @@ -56,9 +65,19 @@ public abstract class SetupMultiPaneFragment extends SetupFragment { return R.layout.fragment_setup_multi_pane; } - abstract protected Fragment getContentFragment(); + abstract protected SetupGuidedStepFragment onCreateContentFragment(); protected boolean needsDoneButton() { return true; } + + @Override + protected int[] getParentIdsForDelay() { + return new int[] {R.id.content_fragment, R.id.guidedactions_list}; + } + + @Override + public int[] getSharedElementIds() { + return new int[] {R.id.guidedactions_background, R.id.done_button_container}; + } } diff --git a/common/src/com/android/tv/common/ui/setup/SetupStep.java b/common/src/com/android/tv/common/ui/setup/SetupStep.java index b91ed6e2..7545906d 100644 --- a/common/src/com/android/tv/common/ui/setup/SetupStep.java +++ b/common/src/com/android/tv/common/ui/setup/SetupStep.java @@ -26,6 +26,7 @@ import android.support.annotation.Nullable; public abstract class SetupStep { private final SetupStep mPreviousStep; private final int mPreviousBackStackRecordCount; + private Fragment mFragment; public SetupStep(FragmentManager fragmentManager, @Nullable SetupStep previousStep) { mPreviousStep = previousStep; @@ -33,27 +34,17 @@ public abstract class SetupStep { } /** - * Returns fragment to represent this step. - */ - protected abstract Fragment onCreateFragment(); - - /** - * Returns whether this step needs to be added to the back stack or not. - * - * <p>The default behavior is to add the fragment to the back stack. + * Creates and Returns a fragment for this step. */ - protected boolean needsToBeAddedToBackStack() { - return true; + public Fragment createFragment() { + mFragment = onCreateFragment(); + return mFragment; } /** - * Returns whether this step needs fragment transition animations or not. - * - * <p>The default value is {@code} true. + * Returns fragment to represent this step. */ - protected boolean needsFragmentTransitionAnimation() { - return true; - } + protected abstract Fragment onCreateFragment(); /** * Executes the given action. @@ -74,4 +65,11 @@ public abstract class SetupStep { public SetupStep getPreviousStep() { return mPreviousStep; } + + /** + * Returns the fragment which represents this step. + */ + public Fragment getFragment() { + return mFragment; + } } diff --git a/common/src/com/android/tv/common/ui/setup/SteppedSetupActivity.java b/common/src/com/android/tv/common/ui/setup/SteppedSetupActivity.java index e454dc49..984c6c7f 100644 --- a/common/src/com/android/tv/common/ui/setup/SteppedSetupActivity.java +++ b/common/src/com/android/tv/common/ui/setup/SteppedSetupActivity.java @@ -21,19 +21,26 @@ import android.app.Fragment; import android.app.FragmentManager.OnBackStackChangedListener; import android.app.FragmentTransaction; import android.os.Bundle; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.view.View; import com.android.tv.common.R; +import com.android.tv.common.ui.setup.animation.SetupAnimationHelper; /** * Stepped setup activity for onboarding screens or setup activity for TIS. + * + * <p>The inherited class should add theme {@code Theme.Setup.GuidedStep} to its definition in + * AndroidManifest.xml. */ public abstract class SteppedSetupActivity extends Activity implements OnActionClickListener { private boolean mStartedInitialStep = false; private SetupStep mStep; + private long mFragmentTransitionDuration; @Override protected void onCreate(Bundle savedInstanceState) { - setTheme(R.style.Theme_Setup_GuidedStep); super.onCreate(savedInstanceState); setContentView(R.layout.activity_stepped_setup); startInitialStep(); @@ -43,14 +50,18 @@ public abstract class SteppedSetupActivity extends Activity implements OnActionC if (mStep != null) { // Need to change step to the previous one if the current step is popped from // the back stack. - if (mStep.needsToBeAddedToBackStack() - && getFragmentManager().getBackStackEntryCount() - <= mStep.getPreviousBackStackRecordCount()) { + if (getFragmentManager().getBackStackEntryCount() + <= mStep.getPreviousBackStackRecordCount()) { mStep = mStep.getPreviousStep(); } } } }); + mFragmentTransitionDuration = getResources().getInteger( + R.integer.setup_fragment_transition_duration); + SetupAnimationHelper.setFragmentTransitionDuration(mFragmentTransitionDuration); + SetupAnimationHelper.setFragmentTransitionDistance(getResources().getDimensionPixelOffset( + R.dimen.setup_fragment_transition_distance)); } /** @@ -84,7 +95,7 @@ public abstract class SteppedSetupActivity extends Activity implements OnActionC } SetupStep step = onCreateInitialStep(); if (step != null) { - startStep(step); + startStep(step, false); mStartedInitialStep = true; } } @@ -92,22 +103,46 @@ public abstract class SteppedSetupActivity extends Activity implements OnActionC /** * Starts next step. */ - protected void startStep(SetupStep step) { + protected FragmentTransaction startStep(SetupStep step, boolean addToBackStack) { mStep = step; - Fragment fragment = step.onCreateFragment(); + Fragment fragment = step.createFragment(); FragmentTransaction ft = getFragmentManager().beginTransaction(); - if (step.needsFragmentTransitionAnimation()) { - ft.setCustomAnimations(SetupFragment.ANIM_ENTER, SetupFragment.ANIM_EXIT, - SetupFragment.ANIM_POP_ENTER, SetupFragment.ANIM_POP_EXIT); + if (fragment instanceof SetupFragment) { + int[] sharedElements = ((SetupFragment) fragment).getSharedElementIds(); + if (sharedElements != null && sharedElements.length > 0) { + Transition sharedTransition = TransitionInflater.from(this) + .inflateTransition(R.transition.transition_action_background); + sharedTransition.setDuration(getSharedElementTransitionDuration()); + SetupAnimationHelper.applyAnimationTimeScale(sharedTransition); + fragment.setSharedElementEnterTransition(sharedTransition); + fragment.setSharedElementReturnTransition(sharedTransition); + for (int id : sharedElements) { + View sharedView = findViewById(id); + if (sharedView != null) { + ft.addSharedElement(sharedView, sharedView.getTransitionName()); + } + } + } } - if (step.needsToBeAddedToBackStack()) { + if (addToBackStack) { ft.addToBackStack(null); } ft.replace(R.id.fragment_container, fragment).commit(); + + return ft; } @Override public void onActionClick(int actionId) { mStep.executeAction(actionId); } + + /** + * Returns the duration of the shared element transition. + * + * <p>It's (exit transition) + (delayed animation) + (enter transition). + */ + private long getSharedElementTransitionDuration() { + return (mFragmentTransitionDuration + SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS) * 2; + } } diff --git a/common/src/com/android/tv/common/ui/setup/animation/CustomTransition.java b/common/src/com/android/tv/common/ui/setup/animation/CustomTransition.java new file mode 100644 index 00000000..58d9b695 --- /dev/null +++ b/common/src/com/android/tv/common/ui/setup/animation/CustomTransition.java @@ -0,0 +1,54 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.view.ViewGroup; + +/** + * Simple custom transition. + */ +public class CustomTransition extends Transition { + private CustomTransitionProvider mTransitionProvider; + + public CustomTransition(CustomTransitionProvider transitionProvider) { + mTransitionProvider = transitionProvider; + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + Animator animator; + if (startValues != null) { + animator = mTransitionProvider.onDisappear(sceneRoot, startValues.view, startValues, + endValues); + } else { + animator = mTransitionProvider.onAppear(sceneRoot, endValues.view, startValues, + endValues); + } + return animator == null ? null : SetupAnimationHelper.applyAnimationTimeScale(animator); + } +} diff --git a/common/src/com/android/tv/common/ui/setup/animation/CustomTransitionProvider.java b/common/src/com/android/tv/common/ui/setup/animation/CustomTransitionProvider.java new file mode 100644 index 00000000..5f492503 --- /dev/null +++ b/common/src/com/android/tv/common/ui/setup/animation/CustomTransitionProvider.java @@ -0,0 +1,43 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.transition.TransitionValues; +import android.view.View; +import android.view.ViewGroup; + +/** + * Provides custom fragment transition animation. + */ +public interface CustomTransitionProvider { + /** + * Create appearing animator. + * + * @see android.transition.Visibility#onAppear + */ + Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues); + + /** + * Create disappearing animator. + * + * @see android.transition.Visibility#onDisappear + */ + Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues); +} + 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 new file mode 100644 index 00000000..0bd9f7b2 --- /dev/null +++ b/common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java @@ -0,0 +1,294 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.transition.Fade; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +/** + * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734) + */ +public class FadeAndShortSlide extends Visibility { + private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator(); + private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator(); + + private static final String PROPNAME_SCREEN_POSITION = + "android_fadeAndShortSlideTransition_screenPosition"; + private static final String PROPNAME_DELAY = "propname_delay"; + + private static final int DEFAULT_DISTANCE = 200; + + private static abstract class CalculateSlide { + /** Returns the translation value for view when it goes out of the scene */ + public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position, + int distance); + } + + private static final CalculateSlide sCalculateStart = new CalculateSlide() { + @Override + public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { + final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final float x; + if (isRtl) { + x = view.getTranslationX() + distance; + } else { + x = view.getTranslationX() - distance; + } + return x; + } + }; + + private static final CalculateSlide sCalculateEnd = new CalculateSlide() { + @Override + public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) { + final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final float x; + if (isRtl) { + x = view.getTranslationX() - distance; + } else { + x = view.getTranslationX() + distance; + } + return x; + } + }; + + private CalculateSlide mSlideCalculator = sCalculateEnd; + private Visibility mFade = new Fade(); + + // TODO: Consider using TransitionPropagation. + private int[] mParentIdsForDelay; + private boolean mDelayChildFound; + private int mDistance = DEFAULT_DISTANCE; + + public FadeAndShortSlide() { + this(Gravity.START); + } + + public FadeAndShortSlide(int slideEdge) { + this(slideEdge, null); + } + + public FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay) { + setSlideEdge(slideEdge); + mParentIdsForDelay = parentIdsForDelay; + } + + @Override + public void setEpicenterCallback(EpicenterCallback epicenterCallback) { + super.setEpicenterCallback(epicenterCallback); + mFade.setEpicenterCallback(epicenterCallback); + } + + private void captureValues(TransitionValues transitionValues) { + View view = transitionValues.view; + int[] position = new int[2]; + view.getLocationOnScreen(position); + transitionValues.values.put(PROPNAME_SCREEN_POSITION, position); + } + + private int getDelayOrder(View view) { + if (mParentIdsForDelay == null) { + return -1; + } + View parentForDelay = findParentForDelay(view); + if (parentForDelay == null || !(parentForDelay instanceof ViewGroup)) { + return -1; + } + mDelayChildFound = false; + return getTransitionTargetIndex((ViewGroup) parentForDelay, view, 0); + } + + private View findParentForDelay(View view) { + if (isParentForDelay(view.getId())) { + return view; + } + View parent = view; + while (parent.getParent() instanceof View) { + parent = (View) parent.getParent(); + if (isParentForDelay(parent.getId())) { + return parent; + } + } + return null; + } + + private boolean isParentForDelay(int viewId) { + for (int id : mParentIdsForDelay) { + if (id == viewId) { + return true; + } + } + return false; + } + + private int getTransitionTargetIndex(ViewGroup parent, View view, int delayIndex) { + int checked = 0; + int count = parent.getChildCount(); + for (int i = 0; i < count; ++i) { + View child = parent.getChildAt(i); + if (child instanceof ViewGroup && !((ViewGroup) child).isTransitionGroup()) { + int result = getTransitionTargetIndex((ViewGroup) child, view, delayIndex); + if (mDelayChildFound) { + return delayIndex + result; + } + delayIndex += result; + checked += result; + } else { + if (child == view) { + mDelayChildFound = true; + return delayIndex; + } + ++delayIndex; + ++checked; + } + } + return checked; + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + mFade.captureStartValues(transitionValues); + captureValues(transitionValues); + int delayIndex = getDelayOrder(transitionValues.view); + if (delayIndex > 0) { + transitionValues.values.put(PROPNAME_DELAY, + delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); + } + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + mFade.captureEndValues(transitionValues); + captureValues(transitionValues); + int delayIndex = getDelayOrder(transitionValues.view); + if (delayIndex > 0) { + transitionValues.values.put(PROPNAME_DELAY, + delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS); + } + } + + public void setSlideEdge(int slideEdge) { + switch (slideEdge) { + case Gravity.START: + mSlideCalculator = sCalculateStart; + break; + case Gravity.END: + mSlideCalculator = sCalculateEnd; + break; + default: + throw new IllegalArgumentException("Invalid slide direction"); + } + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { + if (endValues == null) { + return null; + } + int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION); + int left = position[0]; + float endX = view.getTranslationX(); + float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); + final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues, + left, startX, endX, APPEAR_INTERPOLATOR, this); + mFade.setInterpolator(APPEAR_INTERPOLATOR); + final AnimatorSet set = new AnimatorSet(); + set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues)); + Long delay = (Long ) endValues.values.get(PROPNAME_DELAY); + if (delay != null) { + set.setStartDelay(delay); + } + return set; + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null) { + return null; + } + int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION); + int left = position[0]; + float startX = view.getTranslationX(); + float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance); + final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, + startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this); + mFade.setInterpolator(DISAPPEAR_INTERPOLATOR); + final AnimatorSet set = new AnimatorSet(); + final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues); + fadeAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + fadeAnimator.removeListener(this); + view.setAlpha(0.0f); + } + }); + set.play(slideAnimator).with(fadeAnimator); + Long delay = (Long) startValues.values.get(PROPNAME_DELAY); + if (delay != null) { + set.setStartDelay(delay); + } + return set; + } + + @Override + public Transition addListener(TransitionListener listener) { + mFade.addListener(listener); + return super.addListener(listener); + } + + @Override + public Transition removeListener(TransitionListener listener) { + mFade.removeListener(listener); + return super.removeListener(listener); + } + + @Override + public Transition clone() { + FadeAndShortSlide clone = null; + clone = (FadeAndShortSlide) super.clone(); + clone.mFade = (Visibility) mFade.clone(); + return clone; + } + + @Override + public Transition setDuration(long duration) { + long scaledDuration = SetupAnimationHelper.applyAnimationTimeScale(duration); + mFade.setDuration(scaledDuration); + return super.setDuration(scaledDuration); + } + + /** + * Sets the moving distance in pixel. + */ + public void setDistance(int distance) { + mDistance = distance; + } +} diff --git a/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java new file mode 100644 index 00000000..9d2efcd0 --- /dev/null +++ b/common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java @@ -0,0 +1,208 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.transition.Transition; +import android.transition.TransitionSet; +import android.view.Gravity; +import android.view.View; +import android.widget.ImageView; + +/** + * A helper class for setup animation. + */ +public final class SetupAnimationHelper { + public static final long DELAY_BETWEEN_SIBLINGS_MS = applyAnimationTimeScale(33); + + private static final float ANIMATION_SCALE = 1.0f; + + private static long sFragmentTransitionDuration; + private static int sFragmentTransitionDistance; + + private SetupAnimationHelper() { } + + /** + * Sets the duration of the fragment transition. + */ + public static void setFragmentTransitionDuration(long duration) { + sFragmentTransitionDuration = duration; + } + + /** + * Sets the distance of the fragment transition. + */ + public static void setFragmentTransitionDistance(int distance) { + sFragmentTransitionDistance = distance; + } + + public static class TransitionBuilder { + private int mSlideEdge = Gravity.START; + private int mDistance = sFragmentTransitionDistance; + private long mDuration = sFragmentTransitionDuration; + private int[] mParentIdForDelay; + private int[] mExcludeIds; + + /** + * Sets the edge of the slide transition. + * + * @see android.transition.Slide#setSlideEdge + */ + public TransitionBuilder setSlideEdge(int slideEdge) { + mSlideEdge = slideEdge; + return this; + } + + /** + * Sets the duration of the transition. + */ + public TransitionBuilder setDuration(long duration) { + mDuration = duration; + return this; + } + + /** + * Sets the ID of the view whose descendants will perform delayed move. + * + * @see android.view.ViewGroup#isTransitionGroup + */ + public TransitionBuilder setParentIdsForDelay(int[] parentIdForDelay) { + mParentIdForDelay = parentIdForDelay; + return this; + } + + /** + * Sets the ID's of the views which will not be included in the transition. + */ + public TransitionBuilder setExcludeIds(int[] excludeIds) { + mExcludeIds = excludeIds; + return this; + } + + /** + * Builds and returns the {@link android.transition.Transition}. + */ + public Transition build() { + FadeAndShortSlide transition = new FadeAndShortSlide(mSlideEdge, mParentIdForDelay); + transition.setDistance(mDistance); + transition.setDuration(mDuration); + if (mExcludeIds != null) { + for (int id : mExcludeIds) { + transition.excludeTarget(id, true); + } + } + return transition; + } + } + + /** + * Applies the animation scale to the given {@code animator}. + */ + public static Animator applyAnimationTimeScale(Animator animator) { + if (animator instanceof AnimatorSet) { + for (Animator child : ((AnimatorSet) animator).getChildAnimations()) { + applyAnimationTimeScale(child); + } + } + if (animator.getDuration() > 0) { + animator.setDuration((long) (animator.getDuration() * ANIMATION_SCALE)); + } + animator.setStartDelay((long) (animator.getStartDelay() * ANIMATION_SCALE)); + return animator; + } + + /** + * Applies the animation scale to the given {@code transition}. + */ + public static Transition applyAnimationTimeScale(Transition transition) { + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int count = set.getTransitionCount(); + for (int i = 0; i < count; ++i) { + applyAnimationTimeScale(set.getTransitionAt(i)); + } + } + if (transition.getDuration() > 0) { + transition.setDuration((long) (transition.getDuration() * ANIMATION_SCALE)); + } + transition.setStartDelay((long) (transition.getStartDelay() * ANIMATION_SCALE)); + return transition; + } + + /** + * Applies the animation scale to the given {@code time}. + */ + public static long applyAnimationTimeScale(long time) { + return (long) (time * ANIMATION_SCALE); + } + + /** + * Returns an animator which animate the source image of the {@link ImageView}. + * + * <p>The frame rate is 60 fps. + */ + public static ObjectAnimator createFrameAnimator(ImageView imageView, int[] frames) { + return createFrameAnimatorWithDelay(imageView, frames, 0); + } + + /** + * Returns an animator which animate the source image of the {@link ImageView} with start delay. + * + * <p>The frame rate is 60 fps. + */ + public static ObjectAnimator createFrameAnimatorWithDelay(ImageView imageView, int[] frames, + long startDelay) { + ObjectAnimator animator = ObjectAnimator.ofInt(imageView, "imageResource", frames); + // Make it 60 fps. + animator.setDuration(frames.length * 1000 / 60); + animator.setInterpolator(null); + animator.setStartDelay(startDelay); + animator.setEvaluator(new TypeEvaluator<Integer>() { + @Override + public Integer evaluate(float fraction, Integer startValue, Integer endValue) { + return startValue; + } + }); + return animator; + } + + /** + * Creates a fade out animator. + * + * @param view The view which will be animated. + * @param duration The duration of the animation. + * @param makeVisibleAfterAnimation If {@code true}, the view will become visible after the + * animation ends. + */ + public static Animator createFadeOutAnimator(final View view, int duration, + boolean makeVisibleAfterAnimation) { + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f); + if (makeVisibleAfterAnimation) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setAlpha(1.0f); + } + }); + } + return animator; + } +} diff --git a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java new file mode 100644 index 00000000..99b8811a --- /dev/null +++ b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java @@ -0,0 +1,128 @@ +package com.android.tv.common.ui.setup.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Path; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.view.View; + +import com.android.tv.common.R; + +/** + * This class is used by Slide and Explode to create an animator that goes from the start position + * to the end position. It takes into account the canceled position so that it will not blink out or + * shift suddenly when the transition is interrupted. + * The original class is android.support.v17.leanback.transition.TranslationAnimationCreator which + * is hidden. + */ +// Copied from android.support.v17.leanback.transition.TransltaionAnimationCreator +class TranslationAnimationCreator { + /** + * Creates an animator that can be used for x and/or y translations. When interrupted, it sets a + * tag to keep track of the position so that it may be continued from position. + * + * @param view The view being moved. This may be in the overlay for onDisappear. + * @param values The values containing the view in the view hierarchy. + * @param viewPosX The x screen coordinate of view + * @param startX The start translation x of view + * @param endX The end translation x of view + * @param interpolator The interpolator to use with this animator. + * @return An animator that moves from (startX, startY) to (endX, endY) unless there was a + * previous interruption, in which case it moves from the current position to (endX, + * endY). + */ + static Animator createAnimation(View view, TransitionValues values, int viewPosX, float startX, + float endX, TimeInterpolator interpolator, Transition transition) { + float terminalX = view.getTranslationX(); + Integer startPosition = (Integer) values.view.getTag(R.id.transitionPosition); + if (startPosition != null) { + startX = startPosition - viewPosX + terminalX; + } + // Initial position is at translation startX, startY, so position is offset by that + // amount + int startPosX = viewPosX + Math.round(startX - terminalX); + + view.setTranslationX(startX); + if (startX == endX) { + return null; + } + Path path = new Path(); + path.moveTo(startX, 0); + path.lineTo(endX, 0); + ObjectAnimator anim = + ObjectAnimator.ofFloat(view, View.TRANSLATION_X, View.TRANSLATION_Y, path); + + TransitionPositionListener listener = + new TransitionPositionListener(view, values.view, startPosX, terminalX); + transition.addListener(listener); + anim.addListener(listener); + anim.addPauseListener(listener); + anim.setInterpolator(interpolator); + return anim; + } + + private static class TransitionPositionListener extends AnimatorListenerAdapter + implements Transition.TransitionListener { + + private final View mViewInHierarchy; + private final View mMovingView; + private final int mStartX; + private Integer mTransitionPosition; + private float mPausedX; + private final float mTerminalX; + + private TransitionPositionListener(View movingView, View viewInHierarchy, int startX, + float terminalX) { + mMovingView = movingView; + mViewInHierarchy = viewInHierarchy; + mStartX = startX - Math.round(mMovingView.getTranslationX()); + mTerminalX = terminalX; + mTransitionPosition = (Integer) mViewInHierarchy.getTag(R.id.transitionPosition); + if (mTransitionPosition != null) { + mViewInHierarchy.setTag(R.id.transitionPosition, null); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mTransitionPosition = Math.round(mStartX + mMovingView.getTranslationX()); + mViewInHierarchy.setTag(R.id.transitionPosition, mTransitionPosition); + } + + @Override + public void onAnimationEnd(Animator animator) {} + + @Override + public void onAnimationPause(Animator animator) { + mPausedX = mMovingView.getTranslationX(); + mMovingView.setTranslationX(mTerminalX); + } + + @Override + public void onAnimationResume(Animator animator) { + mMovingView.setTranslationX(mPausedX); + } + + @Override + public void onTransitionStart(Transition transition) {} + + @Override + public void onTransitionEnd(Transition transition) { + mMovingView.setTranslationX(mTerminalX); + } + + @Override + public void onTransitionCancel(Transition transition) {} + + @Override + public void onTransitionPause(Transition transition) {} + + @Override + public void onTransitionResume(Transition transition) {} + } + +} + |