aboutsummaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-12-09 13:48:17 -0800
committerNick Chalko <nchalko@google.com>2015-12-11 15:09:19 -0800
commit1abddd9f6225298066094e20a6c29061b6af4590 (patch)
tree97d701f8681cca9939c86e5e61523775d4c13aea /common
parent7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (diff)
downloadTV-1abddd9f6225298066094e20a6c29061b6af4590.tar.gz
Sync to ub-tv-heroes at 1.08.301
source change id If9b64d7bbc6e8f77b360e502d34e5452775c0402 Change-Id: I4ffe87911cb85e54880d1d918d1b8fb7bb8cfb7d
Diffstat (limited to 'common')
-rw-r--r--common/Android.mk3
-rw-r--r--common/res/drawable/setup_action_button_done.xml5
-rw-r--r--common/res/drawable/setup_selector_background.xml24
-rw-r--r--common/res/layout/fragment_setup_multi_pane.xml51
-rw-r--r--common/res/transition/transition_action_background.xml26
-rw-r--r--common/res/values/colors.xml24
-rw-r--r--common/res/values/dimens.xml37
-rw-r--r--common/res/values/integers.xml2
-rw-r--r--common/res/values/styles.xml82
-rw-r--r--common/res/values/themes.xml12
-rw-r--r--common/src/com/android/tv/common/BooleanSystemProperty.java5
-rw-r--r--common/src/com/android/tv/common/TvCommonConstants.java21
-rw-r--r--common/src/com/android/tv/common/TvCommonUtils.java69
-rw-r--r--common/src/com/android/tv/common/dvr/DvrSessionClient.java149
-rw-r--r--common/src/com/android/tv/common/dvr/DvrTvInputService.java447
-rw-r--r--common/src/com/android/tv/common/dvr/DvrTvView.java67
-rw-r--r--common/src/com/android/tv/common/dvr/DvrUtils.java53
-rw-r--r--common/src/com/android/tv/common/feature/FeatureUtils.java23
-rw-r--r--common/src/com/android/tv/common/feature/GServiceFeature.java5
-rw-r--r--common/src/com/android/tv/common/feature/PropertyFeature.java5
-rw-r--r--common/src/com/android/tv/common/feature/SharedPreferencesFeature.java82
-rw-r--r--common/src/com/android/tv/common/feature/TestableFeature.java89
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupFragment.java187
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java72
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java45
-rw-r--r--common/src/com/android/tv/common/ui/setup/SetupStep.java30
-rw-r--r--common/src/com/android/tv/common/ui/setup/SteppedSetupActivity.java57
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/CustomTransition.java54
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/CustomTransitionProvider.java43
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/FadeAndShortSlide.java294
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/SetupAnimationHelper.java208
-rw-r--r--common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java128
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) {}
+ }
+
+}
+